Tenaciousd93
Tenaciousd93

Reputation: 3488

React re-render anything anytime

I'm new in React, I'm building a web app that display a video and a box with transcription words.

The words can be edited and could have some data to be displayed with tooltips or dialogs.

The main React component download the data from an API and compose the page made of some components: top bar, side bar, player and transcription box.

Once downloaded I store json data in state and pass it into components via props. The component process the json objects (the words) and create a lot of stateless components, the words, with some props too (for example, the tooltips, key prop, the id, the className...) then i put all of them inside an array and return to the transcription box.

What I've notice is that every time I edit the main state of the parent element (not the data used from transcription component) the words are re-rendered (I enabled highlighy updates in react tab of chrome's dev tools).

This makes no problem with a small array of words, like 50-100 elements, but with thousands of items it produce a big latency between user actions and graphical updates.

I'm probably missing the point: what I'm doing wrong with it?

Can you please show me how can I process large amount of JSON object items to react components in the correct way?


EDIT: I cut off the not interested code, leaving the minimal code to reproduce what I meant.

The main component that contains transcription and other components in this example is Info.js. I put the data download by API in a separated file because it's very long data, you can find it there: https://pastebin.com/nw1A391y

Info.js

import React, { Component } from 'react';
import TranscriptionBox from '../partials/TranscriptionBox.js';

class Info extends Component {

    /**
     * Set default props
     * @type {Object}
     */
    static defaultProps = {
        name: 'Info'
    }

    /**
    * Component constructor
    * @param {Object} props [Component props]
    */
    constructor (props) {
        // Make property available in this module
        super(props);

        this.state = {
            task: {
                attachments: [],
                automatic_created: true,
                clips: [],
                creation_user: "https://example.com/",
                end_datetime: "2019-08-02T04:30:03.022",
                id: 498,
                metadata: [],
                owner_user: null,
                source: "https://example.com/",
                source_repr: "Hello",
                start_datetime: "2019-08-02T04:00:03",
                status: "https://example.com/",
                status_repr: "created",
                url: "https://example.com/"
            },
            transcription: [] // NOTE: Put here the transcription form the file attached!
        }
    }

    onWordClick = () => {
        console.log("onWordClick");
        this.setState({
            hello: 'onWordClick'
        })
    }

    onActionChange = () => {
        console.log("action change");
        this.setState({
            hello: 'onActionChange'
        })
    }

    onTaskProgressChange = () => {
        console.log("onTaskProgressChange");
        this.setState({
            hello: 'onTaskProgressChange'
        })
    }

    render() {
        return (<div>
            <TranscriptionBox
                key="transcription_box_component"
                ref="transcription_box_component"
                task={this.state.task}
                transcription={this.state.transcription}
                taskLoaded={true}
                action={null}
                progress={null}
                selected={null}
                isSpecialPressed={false}
                handleActionChange={this.onActionChange}
                handleWordClick={this.onWordClick}
                handleProgressChange={this.onTaskProgressChange}
                />
        </div>);
    }
}

export default Info;

This is the transcription box component, that render API data and make words object inside a scroll box.

TranscriptionBox.js

import React from 'react';
// Import classes and components
import Word from './Word.js';
// Import style
import '../styles/TranscriptionBox.css'

class TranscriptionBox extends React.Component {

    /**
    * Set default props
    * @type {Object}
    */
    static defaultProps = {
        name: 'TranscriptionBox',
        tempData: undefined
    }

    /**
    * Component constructor
    * @param {Object} props [Component props]
    */
    constructor (props) {
        // Make property available in this module
        super(props);
        // Set default state
        this.state = {
            visible: true,
            value: '',
            transcription: this.props.transcription,
            lastPostion: 0,
            inDownloading: false,
            downloaded: false
        }
    }

    /*
    |-------------------------------------------------------------------------|
    |                               ACTION HANDLE                             |
    |-------------------------------------------------------------------------|
    */

    /**
     * Handle click action on transcription box word
     * @param  Event ev     Double click event
     * @return void
     */
    handleWordClick = (ev) => {
        // Get current action
        let action = this.props.action;
        // Get the target of click
        let target = ev.currentTarget;
        // Init word object
        let data = false;
        // Manage wordclick
        this.props.handleActionChange(undefined);
    }

    /*
    |-------------------------------------------------------------------------|
    |                            ELEMENTS GENERATION                          |
    |-------------------------------------------------------------------------|
    */

    /**
     * Call the ReactJS dangerouslySetInnerHTML to transform string into HTML
     * @return {HTML}   Html code
     */
    renderTranscription = () => {
        console.log("*** TB@renderTranscription");
        // Create HTML tag from text transcription
        return this._elaborateTranscription(this.props.transcription);
    }

    /**
     * Elaborate transcription and create all tags
     * // COMBAK: make clips tag manage array of clips
     * @param  array transcription  The transcription to elaborate
     * @return array                The transcription elaborated
     */
    _elaborateTranscription = (transcription) => {
        // Init vars
        let reactWord, word, wordObject, seconds;
        // Init array
        let elaborated = [];

        // If transcription is null or empty transcription
        if (!transcription || transcription.length < 1) {
            // Init reactWord to be pushed
            reactWord = <em key="no-transcription">Loading...</em>;
            // Return a placeholder
            elaborated.push(reactWord);
            // Return elaborated
            return elaborated;
        }

        // Get this context to me var
        let me = this;

        // Iterate each word and take the iterator count
        transcription.forEach(function (transcriptionWord, i) {
            // Create a copy
            wordObject = {...transcriptionWord};

            // If has no id
            if (!transcriptionWord['id']) {
                // Compose an unique id
                wordObject['id'] = "word-" + i;
            }

            // Get seconds
            seconds = parseInt(transcriptionWord['time']);
            // Calculate the seconds at
            wordObject['seconds'] = seconds;


            // Create a new word react element
            word = <Word key={wordObject.id}
                    handleDoubleClick={me.handleWordDoubleClick}
                    handleClick={me.handleWordClick}
                    {...wordObject} />;
            // Push created tag into array
            elaborated.push(word);
        });

        // Return elaborated text
        return elaborated;
    }

    /*
    |-------------------------------------------------------------------------|
    |                                   RENDER                                |
    |-------------------------------------------------------------------------|
    */

    /**
    * Render component
    * @return {} []
    */
    render() {
        return (
            <div
                id="transcription-box"
                ref="transcription-box"
                className='task-transcription-box-container border-secondary'>
                {this.renderTranscription()}
            </div>
        );
    }
}

// Export default component to be accessible in other components
export default TranscriptionBox;

This is the word stateless component.

Word.js

import React from 'react';

import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import { Icon } from 'office-ui-fabric-react/lib/Icon';

const Word  = props => {
    return <div
        key={props.id}
        className='transcription-word'
        data-id={props.id}
        data-metadata=''
        data-clips=''
        data-seconds=''
        data-datetime=''
        onDoubleClick={props.handleDoubleClick}
        onClick={props.handleClick}
        >
            {props.data}&nbsp;
        </div>;
};

export default Word;

I make a short video of the problem. In this gif you can see that each time I press on a word (the method handleClick is called) and an useless prop of Info.js state change. No word data is touched, but chrome show me with light blue border that react has re-render the words.

Example

Upvotes: 0

Views: 470

Answers (1)

Vicente
Vicente

Reputation: 2472

Well from what I understood from your question you are rendering a lot of components unnecessarily?

What you can do for those stateless components is to control when they render. For example, render only when they receive different props. (making them a stateless pure component). You can use memo() for that. For example:

import React, { memo } from "react";
import { isEqual } from "../definedByYou";

const YourComponent = props => {
  return <div>{props.text}</div>;
};

// When text changes (isEqual returns false) => component will render
function arePropsEqual(prevProps, nextProps) {
  return isEqual(prevProps.text, nextProps.text);
}

export default memo(YourComponent, arePropsEqual);

Where isEqual is a function defined by you that returns true when the compared props are the same

EDIT

Acordion to your Word component try:

import React, { memo } from "react";

import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import { Icon } from 'office-ui-fabric-react/lib/Icon';

const Word  = props => {
    return <div
        key={props.id}
        className='transcription-word'
        data-id={props.id}
        data-metadata=''
        data-clips=''
        data-seconds=''
        data-datetime=''
        onDoubleClick={props.handleDoubleClick}
        onClick={props.handleClick}
        >
            {props.data}&nbsp;
        </div>;
};

// if your data prop is an object you can't just use "==="
function arePropsEqual(prevProps, nextProps) {
  return prevProps.id === nextProps.id && prevProps.data === nextProps.data;
}

export default memo(Word, arePropsEqual);

EDIT 2

Using shouldComponentUpdate in your TranscriptionBox component:

shouldComponentUpdate(nextProps, nextState) {
  return true; // Here check differences in state and props :)
}

Upvotes: 1

Related Questions