#Render only children based on nested state

1 messages Β· Page 1 of 1 (latest)

tulip pebble
#

Hey all! I've been posting nearly every day recently, but everyone here is so helpful (especially @pulsar merlin , who has helped me quite a few times, you should give him some karma πŸ’œ !) and I learn a lot whenever I post!

My current issue is that I'd like to get my app to render more selectively. The way it works is I pass in data like the following

const treeData = {
  isOpen: true,
  id: 'parentId',
  children: [
    {
      value: 'React',
      isOpen: true,
      id: 'childId'
      idPath: 'parentID childId'
    },
    //and so on, recursively
  ]
}

and from that data i render a component

function List() {
 return <ListItem data={treeData} />
}

which is a recursive component

export default memo(function ListItem(props) {
  const [isEnter, setIsEnter] = useState(true);
  return (
    <Collapse in={isEnter} appear unmountOnExit>
      <div className="list-item">
        {props.data.value &&
          (props.data.path ? (
            <Link to={props.data.path}>{props.data.value}</Link>
          ) : (
            props.data.value
          ))}
        {props.data.children && props.data.isOpen && (
          <ul>
            {props.data.children.map((li) => {
              return (
                <li
                  key={nanoid()}
                  className={`${li.isOpen ? "opened " : ""} ${
                    li.children && li.children.length > 0 ? "openable" : ""
                  }`}
                  onClick={(e) => props.openList(e, li.idPath)}
                >
                  <ListItem data={li} openList={props.openList} />
                </li>
              );
            })}
          </ul>
        )}
      </div>
    </Collapse>
  );
});

I want each instance of ListItem to only re-render when updated, but they ALL re-render (as does List) with treeData state changes. How can I selectively render ONLY the nested component when the corresponding nested child in the state changes? Thanks!

pulsar merlin
#

The reason why your whole tree renders when there is a state change is of the referential value change.
When it comes to primitive values like string, int, boolean it is very easy to compare those them because we are taking the actual value of the variable defined with these types.

When it comes to objects, we are not storing the value in the variable but a reference (basically pointer). If we compare to variables which are defined as objects with the strict equal, we would compare the reference, not the actual value that's inside the object.

If we copy the any of these values, it is the same thing. For primitives we copy the actual value. If it is an object for example, we copy the reference of the object, and this could some causes confusion!

Primitive example

const name = 'Kirk'
const name2 = name
const name3 = 'Spock'

name === name2 // true
name2 === name3 // false

const num1 = 2
const num2 = num1
const num3 = 7

num1 = num2 // true
num2 = num3 // false

Referencial values:

const myObj = { name: 'James T Kirk', title: 'Captain' }
const myObj2 = myObj
const myObj3 = { name: 'James T Kirk', title: 'Captain' }

myOjb === myObj2 // true -> we copied the reference
myObj2 === myObj3 // false -> note that the `{}` tells JS to create a brand new object. Since we declared an object even with the same value, we get back a different reference

// NOW THE PART THAT COULD CONFUSION PEOPLE
const myObj = { name: 'James T Kirk', title: 'Captain' }
const myObj2 = myObj
// Since we copied the reference of the object if we mutate/change the `myObj2` it will affect the original object
myObj2.name = 'Spock' // myObj and myObj2 are now = { name: 'Spock', title: 'Captain' } 
myObj2.name = 'Science Officer' // myObj and myObj2 are now = { name: 'Spock', title: 'Science Officer' } 
#

So why am I telling you this?
Because I believe this is very important to understand when we are dealing with our data.

This little anecdote explains why we have to create a new object or array when we update our state. Since react does shallow comparison when it comes to property comparison and you did not create a new object but mutated/changed the existing one and returned the same reference, it will not trigger a re-render because react has no way of knowing that something actually happened.

const MyComponent = () => {
  const [state, setState] = useState({ counter: 0 })
  
  const decrease = () => {
    setState(prevState => {
      // We mutate the original array, so the reference does not change, but the actual value does
      prevState.counter--;
      // Return the same reference, it will not trigger a re-render
      return prevState
    })
  }
  
  const increase = () => {
    // We get the reference of the old state
    return setState(prevState => ({
      // We create a new object, and do a shallow copy of the old one
      ...prevState,
      // We increase the counter
      counter: prevState.counter + 1;
    }))
    // Since we have created a new object, we will get the re-render
  }

  return (
    <div>
        <h1>Counter: {state.counter}</h1>
        <button onClick={decrease}>Decrease</button>
        <button onClick={increase}>Increase</button>
    </div>
  )
}
#

Using the component above, if we would click on decrease, we would not see any change, nothing would re-render, but our state would be on -1.
Let's click on increase this time. We will trigger a re-render, but it would seem that our state did not change but in fact it did and now it is back to 0.

Now let's click decrease twice. Nothing happened in terms of the UI, we still see 0 but our actual state is on -2 now.
If you would click on increase button, the state would change, we would get a re-render and we would see the -1 on the screen which would confuse the hell out of us, since we jump from 0 to -1 when we pressed the increase button. It can be very confusing. 🀯

#

Now, that we understand what's happening when we change the state it should be clear by now, that since you change your state which is an object, you will create new reference value(s) for each part of your state.

If you have a nested state like yours and you change the parent, it will tell react to re-render the main and every child/ascendant part of the state just to make sure everything is up to date! This is a very good safety mechanism.

#

@tulip pebble TLDR: You can use React.memo on to memoize your components which are being used. So when you memoize something it means you will keep data in your memory until it is not used anymore, and react will does a prop comparison when it find anything memoized. This should get you the desired functionality.

BUT it is vey important that you remove nanoid() from your rendering, it is not good to have it there. nanoid() is a function, so every time it is being called, it will give you a new uuid. The key prop is there for react to help optimising the re-renders. If you keep changing the key that will tell react that: Oh a prop has changed, I should re-render this component.

You should define your initial state and give an id attribute to your children objects using nanoid() since this is a static data and you will not change this id property anywhere.

tulip pebble
#

wow! this is such a thorough and well written explanation! thank you! i feel much more confident in my understanding of objects now

#

I'll share a zoomed in version of the offending code once ive found it and fixed it!

#

and again, thank you @pulsar merlin for being so helpful in this channel! scrimba should be paying you!

pulsar merlin
#

I am very happy it helped and you feel more confident! πŸ™‚

pulsar merlin
tulip pebble
#

It took me a while to find it, I made sure I was always updating state like

return {
  ...prevState,
  anychanges: 'go here'
}
#

BUT the code that caused my issue was, as you said key={nanoid()}

#

I had thought I went back through to remove it, but I never did. Now my code sets nanoids ONCE in setting state, and never again

#

It seems to me there would rarely be reason not to use React.memo, no?

pulsar merlin
#

Memoization is great, BUT nothing comes for free! πŸ™‚
When you use useCallback, useMemo or React.memo your application will use more memory of the user's device.

#

React and other frameworks as well re-render the components super quickly!
Quick re-rendering is not the problem, slow re-renders are the problem!

#

Until your application visually shows any performance issues, I wouldn't worry much about memoization.

#

obviously, if you have a value which requires heavy computation, you should use useMemo, so it will only run the heavy computation when it is really necessary.

useCallback is also useful if you have a function which needs to be passed into a component's prop.

tulip pebble
#

ah, that makes sense! Thanks for the explanation!