React shallow copy still triggers re-render? - javascript

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.

Related

How to trigger re-render in Parent Component from Child Component

, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)

Updating list with state React

I'm having trouble understanding why a list won't update in React. For my website that I'm building, I'm trying to add a 'favorites' button, but when you click the button it updates the state but the changes never re-render in the list. I tried to make a simpler version, but this doesn't work either:
import React, { useState } from 'react';
import './App.css';
function App() {
const [favorites, setFavorites] = useState([]);
function addFavorite(name, id) {
let newFavorites = favorites;
let newFav = {name: name, id: id};
newFavorites.push(newFav);
setFavorites(newFavorites);
}
return (
<div className="App">
<ul>
{favorites.map((val) => {
return(<li key={val.id}><span>{val.name}</span></li>);
})}
</ul>
<button onClick={() => addFavorite("Thing1", 1)}>Thing 1</button>
<button onClick={() => addFavorite("Thing2", 2)}>Thing 2</button>
<button onClick={() => {console.log(favorites)}}>Check</button>
</div>
);
}
export default App;
I can see the state changes in the console when I log them, but the <ul> element never updates. I've looked online but most of the articles I've found have not been very helpful (I feel the example code I wrote looks a lot like this article.
let newFavorites = favorites;
This assigns newFavorites to point to favorites
newFavorites.push(newFav);
Because newFavorites points to favorites, which is an array in state, you can't push anything onto it and have that change render.
What you need to do, is populate a new array newFavorites with the content of favorites.
Try
const newFavorites = [...favorites];
That should work
I would make some changes in your addFavourite function:
function addFavorite(name, id) {
let newFav = {name, id};
setFavorites([…favourites, newFav]);
}
This way, everytime you click favourite, you ensure a new array is being created with spread operator
Its not working because use are mutating the existing state.
The list is updating but it won't render as useState only renders when the parameter passed to it is different from previous one but in your case though you are changing the list items still the reference is not altering.
To make it work you can use spread operator for lists for even Array.concat() returns a new updated array.
function addFavorite(name, id) {
let newFav = {name: name, id: id};
setFavorites(prev=>[...prev, newFav]);
}
For changing array state, you should use:
function addFavorite(name, id) {
let newFav = { name: name, id: id };
setFavorites((favorites) => [...favorites, newFav]);
}

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

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.

React/Redux rendering a list that's updating every second

I have a react component that receives props from the redux store every second. The new state has an array that's different than the last array. To be specific, every second an element is added to the array. For example:
in one state the array is:
[1, 2, 3, 4, 5, 6]
the next state
[1, 2, 3, 4, 5, 6, 7]
My reducer:
return {
...state,
myList: [ payload, ...state.myList.filter(item => payload.id !== item.id).slice(0, -1) ]
}
Now, in my react component I am subscribing to this state and for every change, the list is re-rendered.
import React, { Component } from 'react';
import MyRow from './MyRow';
class MyList extends Component {
render() {
return (
<div>
{this.props.myList.map((list, index) => (
<MyRow key={list.id} data={list}/>
))}
</div>
);
}
}
function select({ myList }) {
return { myList };
}
export default connect(select)(MyList);
In MyRow.js
import { PureComponent } from 'react';
class MyRow extends PureComponent {
render() {
const data = this.props.data;
return (
<div>
{data.id} - {data.name}
</div>
);
}
}
export default MyRow;
Now, my problem is: It's costly for me to re-render every element that has been already rendered. The MyRow heavily uses styled components and other expensive operations.
This is causing react to re-render the whole list every second when the state is updated. This gets worst if updates come in less than 1 seconds, like 4 updates per second. The react app simply crashes in this case.
Is there any way to only add the newly added item to the list and not re-render the whole list?
Thanks
You're using PureComponent, that do shallow comparison, then your component MyRow should not be rerendered on each new item being added (Please follow my code example below).
Is there any way to only add the newly added item to the list and not re-render the whole list?
According to your question - Yes, using PureComponent should render only 1 time the new item:
Here's what the React's docs says:
If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.
Code example of PureComponent:
You can check out the code sample, that I did for you.
You will see that the Item component is always rendered only 1 time, because we use React.PureComponent. To prove my statement, each time the Item is rendered, I added current time of rendering. From the example you will see that the Item Rendered at: time is always the same, because it's rendered only 1 time.
const itemsReducer = (state = [], action) => {
if (action.type === 'ADD_ITEM') return [ ...state, action.payload]
return state
}
const addItem = item => ({
type: 'ADD_ITEM',
payload: item
})
class Item extends React.PureComponent {
render () {
// As you can see here, the `Item` is always rendered only 1 time,
// because we use `React.PureComponent`.
// You can check that the `Item` `Rendered at:` time is always the same.
// If we do it with `React.Component`,
// then the `Item` will be rerendered on each List update.
return <div>{ this.props.name }, Rendered at: { Date.now() }</div>
}
}
class List extends React.Component {
constructor (props) {
super(props)
this.state = { intervalId: null }
this.addItem = this.addItem.bind(this)
}
componentDidMount () {
// Add new item on each 1 second,
// and keep its `id`, in order to clear the interval later
const intervalId = setInterval(this.addItem, 1000)
this.setState({ intervalId })
}
componentWillUnmount () {
// Use intervalId from the state to clear the interval
clearInterval(this.state.intervalId)
}
addItem () {
const id = Date.now()
this.props.addItem({ id, name: `Item - ${id}` })
}
renderItems () {
return this.props.items.map(item => <Item key={item.id} {...item} />)
}
render () {
return <div>{this.renderItems()}</div>
}
}
const mapDispatchToProps = { addItem }
const mapStateToProps = state => ({ items: state })
const ListContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(List)
const Store = Redux.createStore(itemsReducer)
const Provider = ReactRedux.Provider
ReactDOM.render(
<Provider store={Store}>
<ListContainer />
</Provider>,
document.getElementById('container')
)
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.0/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.26.0/polyfill.min.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
Solutions:
If the performance problem is caused by MyRow rerending, please find out what's the reason of rerending, because it should not happen, because of PureComponent usage.
You can try to simplify your reducer, in order to check / debug, is the reducer causing the problem. For instance, just add the new item to the list (without doing anything else as filtrations, slice, etc): myList: [ ...state.myList, payload ]
Please make sure you always pass the same key to your item component <MyRow key={list.id} data={list} />. If the key or data props are changed, then the component will be rerendered.
Here are some other libraries, these stand for efficient rendering of lists. I'm sure they will give us some alternatives or insights:
react-virtualized - React components for efficiently rendering large lists and tabular data
react-infinite - A browser-ready efficient scrolling container based on UITableView
PureComponent will shallowly compare the props and state. So my guess here is that the items are somehow new objects than the previous passed props, thus the rerendering.
I would advice, in general, to only pass primitive values in pure components :
class MyList extends Component {
render() {
return (
<div>
{this.props.myList.map((item, index) => (
<MyRow key={item.id} id={item.id} name={data.name} />
//or it's alternative
<MyRow key={item.id} {...item} />
))}
</div>
);
}
}
//...
class MyRow extends PureComponent {
render() {
const {id, name} = this.props;
return (
<div>
{id} - {name}
</div>
);
}
}
The problem really exists in the reducer.
myList: [ payload, ...state.myList.filter(item => payload.id !== item.id).slice(0, -1) ]
What is the logic implemented using slice(0,-1)?
It is the culprit here.
From your question I understood the next state after [1,2,3] will be [1,2,3,4].
But your code will be giving [4,1,2], then [5,4,1] then [6,5,4].
Now all the elements in the state are new, not in the initial state. See state is not just getting appended it is completely changing.
Please see if you are getting the desired result by avoiding slice.
myList: [ payload, ...state.myList.filter(item => payload.id !== item.id)]
There is quite an easy solution for this. React VDOM is just a diffing algorithm. The only piece missing with your JSX is something called key which is like an id that the diffing algo uses and renders the particular element. Just tag the element with a KEY something like this https://reactjs.org/docs/lists-and-keys.html#keys
<li key={number.toString()}>
{number} </li>
it looks like you are creating a new array each time in the reducer in which all array indices need to be re-calculated. have you tried appending the new node to the end of the list instead of prepending?

Update Array in React

I am coming from Angular 1.x and looking to update an unordered list with React / Redux.
In console.log, I am seeing the array being updated, but it doesn't seem to bind to the DOM. I have the following --
onKeyPress of an input, I have a function that pushes to messages array.
<ul className="list-inline">
{messages.map(function(message, key){
return (
<li key={key} message={message}>{message}</li>
);
})}
</ul>
Update
I have the following (but no luck yet) Some notes. I am using Firebase to listen for events, and add to an array. Wondering if its a bind issue? --
class Comments extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {messages: this.props.messages};
}
componentDidMount() {
const path = '/comments/all';
// Firebase watches for new comments
firebase
.database()
.ref(path)
.on('child_added', (dataSnapshot) => {
this.state.messages.push(dataSnapshot.val());
this.setState({
messages: this.state.messages
});
//console.log(dataSnapshot.val());
});
}
render() {
const messages = this.state.messages;
return (
<ul className="list-inline">
{messages.map(function(message, key){
<li key={key}>{message}</li>
})}
</ul>
);
}
}
You need messages to be set in the components state.
getInitialState() {
return {
messages: []
}
}
Then in your function, set the state:
this.setState({messages: updatedMessages})
and then map over the messages state, or a messages variable in render:
const messages = this.state.messages;
<ul className="list-inline">
{messages.map(function(message, key){
etc...
put messages array and set state change to render DOM. You should read https://facebook.github.io/react/docs/component-specs.html
Two issues:
You mustn't directly mutate the state object in React (see: Do Not Directly Modify State). Instead, provide a new array via setState with the new entry in it.
When updating state based on existing state, you must use the function callback version of setState, not the version accepting an object, because state updates are asynchronous and may be merged (see: State Updates May Be Asynchronous, though it's really "will," not "may"). Using the object version often happens to work, but it isn't guaranteed to; indeed, it's guaranteed not to, at some point.
Let's look at various ways to update an array:
Adding to the end (appending):
this.setState(({messages}) => ({
messages: [...messages, newValue]
}));
In your case, newValue would be dataSnapshot.val().
(We need the () around the object initializer because otherwise the { would seem to start a full function body instead of a concise expression body.)
Adding to the beginning (prepending):
Largely the same, we just insert the new element in a different place:
this.setState(({messages}) => ({
messages: [newValue, ...messages]
}));
(We need the () around the object initializer because otherwise the { would seem to start a full function body instead of a concise expression body.)
Updating an existing item in the array
Suppose you have an array of objects and want to update one you have in the variable targetElement:
this.setState(({messages}) => {
messages = messages.map(element => {
return element === targetElement
? {...element, newValue: "new value"}
: element;
});
return {messages};
}));
Removing an existing item in the array
Suppose you have an array of objects with id values and want to remove the one with targetId:
this.setState(({messages}) => {
messages = messages.filter(element => element.id !== targetId);
return {messages};
}));
By Index
Warning: Updating arrays in React by index is generally not best practice, because state updates are batched together and indexes can be out of date. Instead, work based on an identifying property or similar.
But if you have to use an index and you know it won't be out of date:
this.setState(({messages}) => {
messages = messages.filter((e, index) => index !== targetindex);
return {messages};
}));

Categories