The application that serves as an example in this presentation is here:
https://github.com/elze/use-selector-blogposts/
It is deployed here:
https://use-selector-blogposts.vercel.app/
https://github.com/elze/redux-toolkit-blogposts
It is deployed here:
https://stackblitz.com/github/elze/redux-toolkit-blogposts on Stackblitz
A global state in an application presents an interesting problem: re-rendering.
In our React.js application all the data is contained in one, single, global state (a JSON object).
Simple example: a web page that displays blog posts.
+-----------------------------------+
| BlogPostsComponent |
| |
| +---------------------------+ |
| | BlogPostComponent 1111 | |
| +---------------------------+ |
| |
| +---------------------------+ |
| | BlogPostComponent 2222 | |
| +---------------------------+ |
| |
| +---------------------------+ |
| | BlogPostComponent 3333 | |
| +---------------------------+ |
+-----------------------------------+
Each blog post can have one of 4 statuses:
Draft
Published
Scheduled for publishing (at a later date)
Removed.
A blog post status is one of its properties, e.g.
{
id: '1111',
title: 'How to prevent re-renderings when using useSelector hook',
blogPostStatus: 'draft',
}
The application state will hold all the blog posts.
If a user changes a blog post status, the application state will change, or rather, the application will transition into a new state.
The application state holds an array of blogposts.
state = {
entities: [
{
id: '1111',
title: 'How to prevent re-renderings when using useSelector hook',
blogPostStatus: 'draft',
},
{
id: '2222',
title: 'How to prevent re-renderings when using Redux-Toolkit',
blogPostStatus: 'draft',
},
{
id: '3333',
title: 'Comparison between Redux and Redux-Toolkit',
blogPostStatus: 'draft',
}
]
};
The useSelector
hook selects the part of the state that a component needs to use.
In our application, we have two components: BlogPostsComponent
, which is the list of all blog posts, and BlogPostComponent
, which represents an individual blogpost.
+-----------------------------------+
| BlogPostsComponent |
| |
| +---------------------------+ |
| | BlogPostComponent 1111 | |
| +---------------------------+ |
| |
| +---------------------------+ |
| | BlogPostComponent 2222 | |
| +---------------------------+ |
| |
| +---------------------------+ |
| | BlogPostComponent 3333 | |
| +---------------------------+ |
+-----------------------------------+
The state for BlogPostsComponent
(plural), is the entire application state -- the whole array. So we would select it with useSelector
like this:
function BlogPostsComponent() {
const blogposts = useSelector((state) => state.entities);
// ... the rest of the component code
}
You would display them like this:
function BlogPostsComponent() {
const blogposts = useSelector((state) => state.entities);
// ...
return (
<div className="App">
{
blogposts?.map((blogpost, ind) => {
return <BlogPostComponent key={blogpost.id} num={ind}/>
})
}
</div>
);
}
In the BlogPostComponent
we use useSelector
to select the child component state.
function BlogPostComponent({ num }) {
const blogPost = useSelector((state) => {return state.entities?.[num]}, shallowEqual);
...
return (
<div className={styles.blogPostsContainer} key={blogPost.id}>
...
<select className={styles.dropDownList} value={blogPost.blogPostStatus}
onChange={(e) => handleChange(blogPost, e)}>
{blogPostStatusOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
);
}
Let's look at the useSelector
for a child component BlogPostComponent
closely.
const blogPost = useSelector((state) => {return state.entities?.[num]}, shallowEqual);
Remember that the state is
state = {
entities: [
{
id: '1111',
title: 'How to prevent re-renderings when using useSelector hook',
blogPostStatus: 'draft',
},
{
id: '2222',
title: 'How to prevent re-renderings when using Redux-Toolkit',
blogPostStatus: 'draft',
}
...
]
};
So state.entities?.[num]
selects one of the entities by its index in the list.
shallowEqual
is a function from React-Redux that compares two items to see if they are equal even if their references are not equal. So it compares the contents of the items, instead of their references.
What happens when we select a new option from the dropdown list in a BlogPostComponent
to change its status? The dropdown list onChange
handler dispatches an action handleChange
:
onChange={(e) => handleChange(blogPost, e)}
The action is processed in the reducer in reducers.js
:
case 'changeStatus':
const id = action.payload.id;
const newBlogPostStatus = action.payload.blogPostStatus;
const ind = state.entities.findIndex((elem) => {
return elem.id === id
}); // finding the index of the element whose status is changing
let blogPost = {...state.entities[ind], blogPostStatus: newBlogPostStatus};
let blogposts = Object.assign([], state.entities, {[ind]: blogPost})
return {entities: blogposts};
How is the new state created?
In reducers.js case 'changeStatus'
:
The spread operator creates blogpost
as a new object with all the fields of the old one, but blogPostStatus
is new
let blogPost = {...state.entities[ind], blogPostStatus: newBlogPostStatus};
The Object.assign
creates a new array, blogposts
, with all the same elements as state.entities
, but the element at the index [ind]
is the new object, blogPost
.
let blogposts = Object.assign([], state.entities, {[ind]: blogPost})
It returns the state as {entities: blogposts}
.
The question now is, how is blogposts
different from state.entities
?
The answer is, their contents are the same, except that the ind
th element of blogposts
has a different blogPostStatus
field than the ind
th element of state.entities
.
This means that for all the elements of blogposts
except the ind
th one , the shallowEqual
should return true.
const blogPost = useSelector((state) => {return state.entities?.[num]}, shallowEqual);
Only for the component whose status we changed, the current state should be different from the previous state.
It would follow that when we change the status of one blog post, only that one component re-renders, and others don't.
Let's see if that's really what happens.
It turns out, no.
In this screencast, when we change the status of one component, all of them re-render.
The flashing component outline shows which components are being re-rendered. It shows up if you have Redux DevTools installed, and if the Devtools are open in your browser.
We see that the outlines of all three blogposts flash every time we change the status of one blogpost.
So all three of them are being re-rendered.
Why is that?
Probably because - based on my Googling - if a parent component's state changes, the parent component will re-render, and this will cause all the children componnent to re-render, even if their state hasn't changed.
And the state of the parent component, BlogPostsComponent
, really has changed.
How would we prevent the state of BlogPostsComponent
from changing?
If the state of BlogPostsComponent
is the whole application state, it can't help but change when one of the blogposts' status changes. But what if the BlogPostsComponent
state was a partial subset of the application state, and that subset never changed?
Such a subset of data are the blogpost IDs. They never change.
And they are completely sufficient for instantiating a child component BlogPostComponent
:
blogposts?.map((blogpost, ind) => {
return <BlogPostComponent key={blogpost.id} num={ind}/>
})
can be changed to
blogpostIds?.map((id, ind) => {
return <BlogPostComponent key={id} num={ind}/>
})
where
blogpostIds = ['1111', '2222', '3333']
In fact, the props we pass to a BlogPostComponent
is only num
- its index in the entities
list in the state.
Even the blogpost's id
only plays a role of the key. React requires keys in list elements, but the id
is not required for creating a BlogPostComponent
. It is looked up only by its index in the list.
This means that the parent component's BlogPostsComponent
state can consist only of the blogposts' IDs, and it won't change when a blogpost's status changes.
So we will change the global application state so that at any given time it will look something like this:
state = {
entities: [
{
id: '1111',
title: 'How to prevent re-renderings when using useSelector hook',
blogPostStatus: 'draft',
},
{
id: '2222',
title: 'How to prevent re-renderings when using Redux-Toolkit',
blogPostStatus: 'draft',
}
...
],
ids: ['1111', '2222']
};
The state will consist of two properties: entities
and ids
.
The entities
are used by a BlogPostComponent
(child component's) selector.
The parent component's BlogPostsComponent
selector will be this:
const blogpostIds = useSelector((state) => state.ids, shallowEqual );
Now we can see the re-renderings with Redux Devtools again:
The fixed code (where neither the parent nor other child components don't re-render when one child component changes state) is in the main
branch of https://github.com/elze/use-selector-blogposts.
The code that has the re-rendering problem is in the 20221106-NoIds
branch of the same repository.
You can also view, fork, edit and run the application code on Stackblitz here: https://stackblitz.com/edit/github-wtcajt.
It is not recommended to use React-Redux
anymore, but to use redux-toolkit
instead.
How do we prevent unnecessary child component re-rendering in redux-toolkit
?
The principle is the same: the parent state consists only of child component IDs, because the IDs don't change. Since the parent component is not re-rendered, other child components (except the one whose state changes) are not re-rendered either.
Here is an application that illustrates this:
https://github.com/elze/redux-toolkit-blogposts
It is deployed here:
https://stackblitz.com/github/elze/redux-toolkit-tickets on Stackblitz
The parent state will be defined by a Typescript interface
export interface BlogPostsState {
entities: BlogPost[],
ids: number[],
loading: boolean
}
This is almost identical to the state of the previously discussed application, use-selector-blogposts
, described on page 18 of this application, except we added a loading
property. This property is not essential, it just provides a nicer UX for our users.
The initial state will be
const initialState: BlogPostsState = {
entities: [],
ids: [],
loading: false
}
To select the parent state - the state of the BlogPostsComponent
- we write a selector
export const selectBlogPostsIds = (state: RootState) =>
<BlogPostsIds>{ids: state.blogposts.ids, loading: state.blogposts.loading};
Since this application is written in Typescript, the variables have types.
The parent component state has a type BlogPostsIds
.
export interface BlogPostsIds {
ids: number[],
loading: boolean
}
In the parent component, you select the state like this:
const { ids, loading } = useSelector(selectBlogPostsIds, shallowEqual)
And you instantiate child components BlogPostComponent
like this:
return (
<div>
<h2>Blog Posts</h2>
<div className={styles.blogPostsContainer}>
<div><b>ID</b></div><div><b>Title</b></div><div><b>Status</b></div><div><b>Move to</b></div>
</div>
{
ids && ids.length > 0 ?
ids.map((id: number, ind: number) => (
<BlogPostComponent key={id} num={ind}/>
))
: <h3>No posts found </h3>
}
</div>
)
To write the selector for the child component state, BlogPostComponent
, remember that a BlogPostComponent
is defined by a num
prop, representing its index in blogposts list.
export default function BlogPostComponent({num}: {num: number}) {
const blogPost = useSelector(selectBlogPost(num), shallowEqual);
...
return (
<div className={styles.blogPostsContainer} key={blogPost.id}>
<div>{blogPost.id}</div><div>{blogPost.title}</div><div>{statusName}</div>
<div>
<select value={blogPost.blogPostStatus} onChange={(e) => handleChange(blogPost, e)}>
{blogPostStatusOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
</div>)
}
So its selector will be
export const selectBlogPost = (id: number) => (state: RootState) => state.blogposts.entities[id];
And now again we can see that only one child element is re-rendered when we change the state of the child element.