I haven't been able to find a clear answer to this, hope this isn't repetitive.
I am using React + Redux for a simple chat app. The app is comprised of an InputBar, MessageList, and Container component. The Container (as you might imagine) wraps the other two components and is connected to the store. The state of my messages, as well as current message (the message the user is currently typing) is held in the Redux store. Simplified structure:
class ContainerComponent extends Component {
...
render() {
return (
<div id="message-container">
<MessageList
messages={this.props.messages}
/>
<InputBar
currentMessage={this.props.currentMessage}
updateMessage={this.props.updateMessage}
onSubmit={this.props.addMessage}
/>
</div>
);
}
}
The issue I'm having occurs when updating the current message. Updating the current message triggers an action that updates the store, which updates the props passing through container and back to the InputBar component.
This works, however a side effect is that my MessageList component is getting re-rendered every time this happens. MessageList does not receive the current message and doesn't have any reason to update. This is a big issue because once the MessageList becomes big, the app becomes noticeably slower every time current message updates.
I've tried setting and updating the current message state directly within the InputBar component (so completely ignoring the Redux architecture) and that "fixes" the problem, however I would like to stick with Redux design pattern if possible.
My questions are:
If a parent component is updated, does React always update all the direct children within that component?
What is the right approach here?
If a parent component is updated, does React always update all the direct children within that component?
By default, yes. This helps avoid subtle bugs for newcomers and is generally a good approach for small components because reconciliation will prevent the DOM from being updated if nothing has changed.
To prevent your sub-component from re-rendering unnecessarily, you need to wrap your function component1 in React.memo()
. By default, React.memo()
will use Object.is()
to compare all of the props values for changes, which works in most cases.
If your props are object values that change identity but not value, then you need to pass an arePropsEqual
function as the second argument. That could be as simple as comparing IDs, like this (using lodash for brevity):
const ContainerComponent = memo(
...,
(oldProps, newProps) => {
const oldMessageIds = _.map(oldProps.messages, 'id');
const newMessageIds = _.map(newProps.messages, 'id');
return _.isEqual(oldMessageIds, newMessageIds);
}
);
If your props are more complicated then your arePropsEqual
function will be more complicated. Common practice is to treat values as immutable so that the default React.memo()
call with no second argument does what you need.
1: In the years since I've answered this question functional components have become the standard in the React ecosystem, so I've updated the answer. If you're still using class components then you'll want to look at shouldComponentUpdate()
, which is similar but the checked is inverted and the default logic is to always return true
instead of using Object.is()
.