React: how to compare current props.children with new one

11,196

Solution 1

You can make use of child key prop that React suggests that arrays of children should be given to uniquely identify them. Because each child has a key, you can reliably tell whether children has changed across prop changes (this is the entire point of keys!). If the keys don't match between new and old then they have changed.

React.render(<App><Child key='1'/><Child key='2'/></App>, document.body)

and do the check in the App component you want to check before each update if the children changed

shouldComponentUpdate(nextProps){
   var oldKeys = this.props.children.map( child => child.key);
   var newKeys = nextProps.children.map( child => child.key);

   //compare new and old keys to make sure they are the same
}

Note that this doesn't tell you if the content of each child has changed, you have to compare each by some criteria (such as deeply comparing props) if you want to know if nothing in the whole tree below this point has changed

as an even further optimization we know that children will never change as result of a state change so we can actually do our comparing in componentWillReceiveProps() and just set some state property like childrenHaveChanged

Solution 2

Something about the design and behavior you describe is off. You should rarely, if ever, have to concern yourself with performing manual diffs of children. That should be left to React. If the library you are using is choking every time the component updates, regardless of whether it's because of children or some other state/prop change, it is not written reactively and you should look for a different one that is written reactively. Or you could contribute to fixing that behavior in the open source project, either by opening an issue or submitting a pull request.

It's not easy to do what you're trying to do because it shouldn't be necessary. React is very, very good at handling reconciliation and will only render when something has actually changed that will change the state of the DOM relevant to it.

Solution 3

As Matt S pointed out, the accepted answer is kind of a fragile workaround and would depend on a non-standard use of key. Aside from the list examples he listed, even using something like key={id} would fall down if your ids remained the same but certain fields were modified in the resources they represent.

This issue contains a good discussion on the topic and ends with a more stable workaround. Essentially, you can simplify the children prop in a way that allows you to run a deep comparison. You can use the React.Children utilities to write the simplification methods:

// Flattens all child elements into a single list
const flatten = (children, flat = []) => {
    flat = [ ...flat, ...React.Children.toArray(children) ]

    if (children.props && children.props.children) {
        return flatten(children.props.children, flat)
    }

    return flat
}

// Strips all circular references and internal fields
const simplify = children => {
    const flat = flatten(children)

    return flat.map(
        ({
            key,
            ref,
            type,
            props: {
                children,
                ...props
            }
        }) => ({
            key, ref, type, props
        })
    )
}

Then you can use shouldComponentUpdate or React.memo to prevent re-renders:

const MyComponent = ({ children }) => (
    <div>{ children }</div>
)

export default React.memo(MyComponent, (prev, next) => (
    JSON.stringify(simplify(prev.children)) ===
    JSON.stringify(simplify(next.children))
))

These utilities + JSON.stringify are just one approach, the one mentioned in the comment is similar and you could also leverage utilities like lodash.isequal for the deep comparison. Unfortunately I don't know of any one or two liners for this comparison but please comment if you know a simpler stable way to do this!

Share:
11,196

Related videos on Youtube

Schovi
Author by

Schovi

Updated on September 14, 2022

Comments

  • Schovi
    Schovi over 1 year

    Hi,

    i am building component which acts only as wrapper for some other generated content and uses third party library. This library works with props.children of component. So far so good, but this thrird party library is little laggy when applied, or refreshed on element. And because only reason to refresh this library is when props.children changed I am trying to figure how to compare this.props.children and nextProps.children in shouldComponentUpdate. I was thinking that PureRenderMixin should do the work, but for me it does not works. Component is rerendered even if I change only state.listName as it is in example below.

    <div>
      List name '{this.state.listName}'
      <br />
      <MyComponent>
        <ul>
          {listOfLi}
        </ul>
      </MyComponent>
    </div>
    

    Is there any way, how to manage comparing of props.children or any other option how to do something like that? Thanks for any help!

  • Schovi
    Schovi about 9 years
    Elegant solution, but does not fully works for me. I have to check inner components too. I tried to make function, which traverse all childrens and their childrens and generate some kind of footprint from (div, span ...), className and style. Properties which has effect on dom position, sizing etc. But i am still not satisfied. I guess i will have to do update manually
  • rbonick
    rbonick almost 7 years
    This also fails to work for a single root child element.
  • Matt S
    Matt S over 6 years
    This answer inaccurate. The key prop is used by React to efficiently manage updates caused by manipulating a list of items, such as by insertions, deletions, and reorders. Comparing key values will tell you only if your collection is composed of the the same number of objects with those key values. The objects themselves could be completely different data.
  • galki
    galki about 6 years
    with React16 you can wrap the listOfLi with a Fragment and put a key on that and then just compare Fragment keys