How to avoid unmounting of children components when using JSX's map? - javascript

This is a more concise version of a question I raised previously. Hopefully, it's better explained and more understandable.
Here's a small app that has 3 inputs that expect numbers (please disregard that you can also type non-numbers, that's not the point). It calculates the sum of all displayed numbers. If you change one of the inputs with another number, the sum is updated.
Here's the code for it:
import { useCallback, useEffect, useState } from 'react';
function App() {
const [items, setItems] = useState([
{ index: 0, value: "1" },
{ index: 1, value: "2" },
{ index: 2, value: "3" },
]);
const callback = useCallback((item) => {
let newItems = [...items];
newItems[item.index] = item;
setItems(newItems);
}, [items]);
return (
<div>
<SumItems items={items} />
<ul>
{items.map((item) =>
<ListItem key={item.index} item={item} callback={callback} />
)}
</ul>
</div>
);
}
function ListItem(props) {
const [item, setItem] = useState(props.item);
useEffect(() => {
console.log("ListItem ", item.index, " mounting");
})
useEffect(() => {
return () => console.log("ListItem ", item.index, " unmounting");
});
useEffect(() => {
console.log("ListItem ", item.index, " updated");
}, [item]);
const onInputChange = (event) => {
const newItem = { ...item, value: event.target.value };
setItem(newItem);
props.callback(newItem);
}
return (
<div>
<input type="text" value={item.value} onChange={onInputChange} />
</div>);
};
function SumItems(props) {
return (
<div>Sum : {props.items.reduce((total, item) => total + parseInt(item.value), 0)}</div>
)
}
export default App;
And here's the console output from startup and after changing the second input 2 to 4:
ListItem 0 mounting App.js:35
ListItem 0 updated App.js:43
ListItem 1 mounting App.js:35
ListItem 1 updated App.js:43
ListItem 2 mounting App.js:35
ListItem 2 updated App.js:43
ListItem 0 unmounting react_devtools_backend.js:4049:25
ListItem 1 unmounting react_devtools_backend.js:4049:25
ListItem 2 unmounting react_devtools_backend.js:4049:25
ListItem 0 mounting react_devtools_backend.js:4049:25
ListItem 1 mounting react_devtools_backend.js:4049:25
ListItem 1 updated react_devtools_backend.js:4049:25
ListItem 2 mounting react_devtools_backend.js:4049:25
As you can see, when a single input is updated, all the children are not re-rendered, they are first unmounted, then re-mounted. What a waste, all the input are already in the right state, only the sum needs to be updated. And imagine having hundreds of those inputs.
If it was just a matter of re-rendering, I could look at memoization. But that wouldn't work because callback is updated precisely because items change. No, my question is about the unmounting of all the children.
Question 1 : Can the unmounts be avoided ?
If I trust this article by Kent C. Dodds, the answer is simply no (emphasis mine) :
React's key prop gives you the ability to control component instances.
Each time React renders your components, it's calling your functions
to retrieve the new React elements that it uses to update the DOM. If
you return the same element types, it keeps those components/DOM nodes
around, even if all* the props changed.
(...)
The exception to this is the key prop. This allows you to return the
exact same element type, but force React to unmount the previous
instance, and mount a new one. This means that all state that had
existed in the component at the time is completely removed and the
component is "reinitialized" for all intents and purposes.
Question 2 : If that's true, then what design should I consider to avoid what seems unnecessary and causes issues in my real app because there's asynchronous processing happening in each input component?

As you can see, when a single input is updated, all the children are
not re-rendered, they are first unmounted, then re-mounted. What a
waste, all the input are already in the right state, only the sum
needs to be updated. And imagine having hundreds of those inputs.
No, the logs you see from the useEffect don't represent a component mount/unmount. You can inspect the DOM and verify that only one input is updated even though all three components get rerendered.
If it was just a matter of re-rendering, I could look at memoization.
But that wouldn't work because the callback is updated precisely because
items change. No, my question is about the unmounting of all the
children.
This is where you would use a functional state update to access the previous state and return the new state.
const callback = useCallback((item) => {
setItems((prevItems) =>
Object.assign([...prevItems], { [item.index]: item })
);
}, []);
Now, you can use React.memo as the callback won't change. Here's the updated demo:
As you can see only corresponding input logs are logged instead of all three when one of them is changed.

At first let's clarify some terminology:
A "remount" is when React deletes it's internal representation of the component, namely the children ("hidden DOM") and the states. A remount is also a rerender, as the effects are cleaned up and the newly mounted component is rendered
A "rerender" is when React calls the render method or for functional components the function itself again, compares the returned value to the children stored, and updates the children based on the result of the previous render
What you observe is not a "remount", it is a "rerender", as the useEffect(fn) calls the function passed on every rerender. To log on unmount, use useEffect(fn, []). As you used the key property correctly, the components are not remounted, just rerendered. This can also easily be observed in the App: the inputs are not getting reset (the state stays).
Now what you want to prevent is a rerender if the props do not change. This can be achieved by wrapping the component in a React.memo:
const ListItem = React.memo(function ListItem() {
// ...
});
Note that usually rerendering and diffing the children is usually "fast enough", and by using React.memo you can introduce new bugs if the props are changed but the component is not updated (due to an incorrect areEqual second argument). So use React.memo rather conservatively, if you have performance problems.

Related

React shallow copy still triggers re-render?

From what I learnt about React, you should not mutate any objects otherwise React doesn't know to re-render, for example, the following example should not trigger re-render in the UI when button is clicked on:
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App({ input }) {
const [items, setItems] = useState(input);
return (
<div>
{items.map((item) => (
<MyItem item={item}/>
))}
<button
onClick={() => {
setItems((prevItems) => {
return prevItems.map((item) => {
if (item.id === 2) {
item.name = Math.random();
}
return item;
});
});
}}
>
Update wouldn't work due to shallow copy
</button>
</div>
);
}
function MyItem ({item}) {
const name = item.name
return <p>{name}</p>
}
ReactDOM.render(
<App
input={[
{ name: "apple", id: 1 },
{ name: "banana", id: 2 }
]}
/>,
document.getElementById("container")
);
You can try above code here
And the correct way to update the array of objects should be like below (other ways of deep copying would work too)
setItems((prevItems) => {
return prevItems.map((item) => {
if (item.id === 2) {
# This way we return a deepcopy of item
return {...item, name: Math.random()}
}
return item;
});
});
Why does the 1st version works fine and the UI is updated right away even though I'm just updating the original item object?
Render happens due to .map that creates new array. If you do something like prev[1].name = "x"; return prev; in your hook, this will not perform an update. Per reactjs doc on setState with function argument:
If your update function returns the exact same value as the current
state, the subsequent rerender will be skipped completely.
Update.
Yes, speaking of parent-child interaction, item would be the same (by reference), but the child props would differ. You have MyItem({ item }) and this item is being destructured from props, say MyItem(props), and this props changes because of the parent source is changed.
So each time you map the list, you explicitly ask the parent to render its children, and the fact that some (or all) of the child's param is not changed does not matter. To prove this you may remove any params from the child component:
{items.map(() => ( <MyItem /> ))}
function MyItem () {
return <p>hello</p>
}
MyItem will be invoked each time you perform items update via state hook. And its props will always be different from the previous version.
if your setItem setting a new object, your page with a state change will re-render
in your logic, you performed a shallow copy, which:
a shallow copy created a new object, and copy everything of the 1st level from old object.
Shallow copy and deep copy also created a new object, they both trigger a re-render in React.
The different of shallow copy and deep copy is: from 2nd level of the old object, shallow copy remains the same objects, which deep copy will create new objects in all level.

useState and changes in the props

I'm trying to understand what happens when you have both props and useState in one component.
I wrote little example of it which has one parent component that prints its numbers with another child component -
const MyNumbers = (props) => {
const [numbers, setNumbers] = useState([...props.arr]);
function changeNumbers() {
setNumbers((nums) => [...nums.map(() => Math.floor(Math.random() * 10))]);
}
return (
<div className="MyNumbers">
<div>
<button onClick={changeNumbers}>Chane numbers</button>
</div>
<div>
{numbers.map((num, idx) => (
<SingleNumber key={idx} num={num}></SingleNumber>
))}
</div>
</div>
);
};
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
});
return <h3>The number is {num}</h3>;
};
Here is the above demo
The SingleNumber component uses useState and as you can see clicking on the "Change numbers" action doesn't change the values in the children component.
But when I wrote almost the same code but now SingleNumber doesn't use useState then clicking on the "Change numbers" changes all the values in the children component (like in this demo).
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed ?
OFC the component "rerenders" when the props change, the useEffect hook in SingleNumber is showing you that the "render phase" is run each time the props change.... effects are run each time the component is rendered.
const SingleNumber = (props) => {
const [num] = useState(props.num);
useEffect(() => {
console.log("useEffect called"); // <-- logged each time the component renders
});
return <h3>The number is {num}</h3>;
};
If you added a dependency on props.num and updated the local state (don't actually do this, it's an anti-pattern in react!), you'll see the UI again update each time the props update.
To answer your queston:
Is it correct to say that a function component with a useState renders
once and then only changed if the state changed but not if the props
changed?
No, this is not technically correct to say if "render" to you means strictly react rendered the component to compute a diff, react components rerender when state or props update. Yes, if "render" more generally means you visually see the UI update.
When you call useState it returns an array with two values in it:
The current value of that bit of the state
A function to update the state
If there is no current value when it sets the state to the default value and returns that.
(The default value is the argument you pass to useState).
If you change the values of props in your example, then the component rerenders.
useState returns the current value of that bit of the state. The state has a value, so it doesn't do anything with the argument you pass to useState. It doesn't matter that that value has changed.
Since nothing else has changed in the output, the rerendered component doesn't update the DOM.
Is it correct to say that a function component with a useState renders once and then only changed if the state changed but not if the props changed?
No, it does rerender but doesn't commit the changes.
When parent component MyNumbers re-renders by clicking changeNumbers, by default (unless React.memo used) all its children components (like SingleNumber) will be re-render.
Now when SingleNumber rerenders, notice useState docs.
During the initial render, the returned state (state) is the same as the value passed as the first argument (initialState).
You initial the state useState(props.num) but it can only be changed by calling the setter function, therefore the state num won't change because you never calling the setter.
But it will rerender on parent render as mentioned above (notice the useEffect logs).
You don't need to use useState in SingleNumber.
because useState called only once when it rendered.
const SingleNumber = (props) => {
// const [num] = useState(props.num);
// useEffect(() => {
// console.log("useEffect called");
// });
return <h3>The number is {props.num}</h3>;
};
if you want to use useState, you can use like this.
const SingleNumber = (props) => {
const [num, setNum] = useState(props.num);
useEffect(() => {
console.log("useEffect called");
setNum(props.num);
}, [props.num]);
return <h3>The number is {num}</h3>;
};

What gets updated in React?

I am new to React and I am a little confused about what gets updated every time the state or props change. For instance:
const Foo = props => {
const [someState, setSomeState] = useState(null)
let a
useEffect(() => {
a = 'Some fetched data'
}, [])
}
Now if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
To understand how the state affect your component, you could check this example on Stackblitz. It shows you how the local variable of your component act when a state changes. This behavior is exactly the same when the component receive a new prop. Here is the code just in case :
import React, { Component } from "react";
import { render } from "react-dom";
const App = () => {
const [state, setState] = React.useState('default');
// see how the displayed value in the viewchanges after the setTimeout
let a = Math.floor(Math.random() * 1000);
React.useEffect(() => {
a = "new a"; // Does nothing in the view
setTimeout(() => setState("hello state"), 2000);
}, []);
return (
<div>
{a} - {state}
</div>
);
};
render(<App />, document.getElementById("root"));
What you can see is :
a is initialized with a random value and it is displayed in the view
useEffect is triggered. a is edited but does not re-render your component
After 2 seconds, setState is called, updating the state state
At this point, a value has changed for a new one because your component is re-rendered after a state update. state also changed for what you gave to it
So for your question :
if the state (i.e. someState) or props get updated, does it run through the function again, making a undefined? Does only JSX elements that depend on the state/props and the hooks that use them get affected? What changes exactly?
The answer is yes, it does run through the function again, and the state/props that has been updated are updated in the JSX. Local variable like a will be set to their default value.

React when pass data to child throw props data duplicate

I'm create Activities function component and call child function component called Categories when i send categories list to Categories function component and log "props" data send twice first one is empty and second has data as follow
Activies
function Activities() {
const [category, setCategory] = useState([]);
function handelChange({ target }) {
setCategory({
...category,
[target.name]: target.value,
});
}
useEffect(() => {
getCategories().then((_categories) => setCategory(_categories));
}, []);
return (<Categories category={category} onChange={handelChange} />)
}
and categories component
function Categories(props) {
console.log(props);
return (<div></div>)
}
i'm trying to log props in useEffect but problem still exist
This is happening because of how the life cycle in React works. This is correct and expected behavior. Before you load the categories, it is a blank array on the initial render. Then it gets the categories, updates the state, and re-renders, this time with categories.
renders with the initial state(empty)
goes and fetches categories
re-renders with the categories
This is entirely expected. That double log is the initial render and then the updated state render. Remember React is a heavily async library. The useEffect doesn't happen during render, it happens after render. Every state update will also cause an update and thus another log. It might be helpful to research what will cause a React render and how the life cycle behaves.
I think you handleChange function should update item in a array of object not change the state object completely
function handelChange({ target: {name, value} }) {
setCategory(categories => {
const categoryIndex = categories.findIndex(pr => pr.id === id);
const category = categories[categoryIndex];
return [
...categories.slice(0, categoryIndex)),
{
...category,
[name]: value,
},
...categories.slice(categoryIndex + 1))
]);
}

shouldComponentUpdate equivalent for functional component, to ignore state changes

My code has a component that takes both props and has its own internal state.
The component should rerender ONLY when its props change. State changes should NOT trigger a rerender.
This behaviour can be implemented with a class based component and a custom shouldComponentUpdate function.
However, this would be the first class based component in the codebase. Everything is done with functional components and hooks.
Therefore I would like to know whether it is possible to code the desired functionality with functional components.
After a few answers that didn't approach the real problem, I think I have to reformulate my question. Here is a minimal example with two components:
Inner takes a prop and has state. This is the component in question. It must not rerender after state changes. Prop changes should trigger a rerender.
Outer is a wrapper around inner. It has no meaning in the scope of this question and is only there to give props to Inner and to simulate prop changes.
To demonstrate the desired functionality I have implemented Inner with a class based component. A live version of this code can be found on codesandbox. How can I migrate it to a functional component:
Inner.tsx:
import React, { Component } from 'react'
interface InnerProps{outerNum:number}
interface InnerState{innerNum:number}
export default class Inner extends Component<InnerProps, InnerState> {
state = {innerNum:0};
shouldComponentUpdate(nextProps:InnerProps, nextState:InnerState){
return this.props != nextProps;
}
render() {
return (
<button onClick={()=>{
this.setState({innerNum: Math.floor(Math.random()*10)})
}}>
{`${this.props.outerNum}, ${this.state.innerNum}`}
</button>
)
}
}
Outer.tsx:
import React, { useState } from "react";
import Inner from "./Inner";
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
<button
onClick={() => {
setOuterState(Math.floor(Math.random() * 10));
}}
>
change outer state
</button>
<Inner outerNum={outerState}></Inner>
</>
);
}
The official docs say to wrap the component in React.memo. But this doesn't seem to work for preventing rerenders on state change. It only applies to prop changes.
I have tried to make React.memo work. You can see a version of the code with both Outer and Inner being functional components here.
Related questions:
How to use shouldComponentUpdate with React Hooks? : This question only deals with prop changes. The accepted answer advises to use React.memo
shouldComponentUpdate in function components : This question predates stateful functional components. The accepted answer explains how functional components don't need shouldComponentUpdate since they are stateless.
React memo do not stop state changes
React.memo only checks for prop changes. If your function component
wrapped in React.memo has a useState or useContext Hook in its
implementation, it will still rerender when state or context change.
Ref:- https://reactjs.org/docs/react-api.html#reactmemo
Your Inner component depends on the property num of the Outer component, you can't prevent it from rendering on property change as React.memo makes properties comparison:
// The default behaviour is shallow comparison between previous and current render properties.
const areEqual = (a, b) => a.num === b.num;
export default React.memo(Inner, areEqual);
By memoizing the Inner component and removing the num dependency, it won't render on Outer rendering, see sandbox attached.
export default function Outer() {
const [outerState, setOuterState] = useState(1);
return (
<>
...
// v Inner is memoized and won't render on `outerState` change.
<Inner />
</>
);
}
If you want to implement shouldComponentUpdate with hooks you can try:
const [currState] = useState();
// shouldUpdateState your's custom function to compare and decide if update state needed
setState(prevState => {
if(shouldUpdateState(prevState,currState)) {
return currState;
}
return prevState;
});
React is by design driven by setState -> re-render loop. Props change is in fact a setState somewhere in parent components. If you don't want the setState to trigger a re-render, then why in the first place use it?
You can pull in a const state = useRef({}).current to store your internal state instead.
function InnerFunc(props) {
const state = useRef({ innerNum: 0 }).current;
return (
<button
onClick={() => {
state.innerNum = Math.floor(Math.random() * 10);
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
}
That said, it's still a valid question to ask: "how to implement shouldComponentUpdate in a react hook fashion?" Here's the solution:
function shouldComponentUpdate(elements, predicate, deps) {
const store = useRef({ deps: [], elements }).current
const shouldUpdate = predicate(store.deps)
if (shouldUpdate) {
store.elements = elements
}
store.deps = deps
return store.elements
}
// Usage:
function InnerFunc(props) {
const [state, setState] = useState({ innerNum: 0 })
const elements = (
<button
onClick={() => {
setState({ innerNum: Math.floor(Math.random() * 10) });
}}
>
{`${props.outerNum}, ${state.innerNum}`}
</button>
);
return shouldComponentUpdate(elements, (prevDeps) => {
return prevDeps[0] !== props
}, [props, state])
}
Noted that it's impossible to prevent a re-render cycle when setState is called, the above hook merely makes sure the re-rendered result stays the same as prev rendered result.
you should use the event that provide the browser and capture in the function before setState, like this
function setState = (e) =>{ //the e is the event that give you the browser
//changing the state
e.preventDefault();
}

Categories