Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to our next lesson on performance optimization in Zustand.
In our last session, we explored derived state. We successfully tackled the problem of unnecessary re-renders by using the shallow equality function, but we left one issue unresolved: the problem of unnecessary computation. Selectors that perform calculations run on every store update, even if the data they depend on hasn't changed.
Today, we'll solve that final piece of the puzzle. This lesson directly addresses the learning outcome: Integrate reselect with a Zustand store to create efficient, memoized selectors for complex derived data.
We will cover:
- The concept of memoization and why it's crucial for expensive calculations.
- How to use the
reselectlibrary to create memoized selectors. - How to seamlessly integrate these selectors with your Zustand store and React components.
1. The Problem: Expensive, Repetitive Computations
As a quick recap, consider this selector from our previous lesson:
const incompleteTodos = useTodoStore(
(state) => state.todos.filter((todo) => !todo.completed)
);
We established that this filter operation runs every single time any part of the store changes. If you have a large todos array or a more complex derivation, this can be a significant and wasteful performance cost.
The solution is memoization. This is an optimization technique where you cache the result of a function call. The next time the function is called with the same inputs, it returns the cached result instead of re-computing it.
For state management, this means we want our selector to re-calculate the derived data only when the specific pieces of state it depends on have actually changed.
2. Introducing reselect
While you could implement memoization yourself or use React's useMemo hook, a more robust and reusable solution is to use a dedicated library. reselect is the de-facto standard for this in the React ecosystem. Although it originated with Redux, it is a standalone library that works perfectly with Zustand.
Let's start by understanding why a library like reselect is a good choice.
Best approach for selectors · pmndrs zustand
This GitHub discussion for Zustand, featuring one of the library's maintainers, directly addresses the question of handling derived data. It confirms that reselect is a recommended approach for avoiding unnecessary calculations.
In the provided GitHub discussion, find the comment by 'timeturnback' starting with '@dai-shi show how exactly...'. Read this comment and the reply from the maintainer 'dai-shi'. Focus on how they frame the problem (.filter() running every time) and why reselect is proposed as the solution (to avoid extra calculation).
As the discussion highlights, the core issue is preventing the calculation itself. Zustand's built-in optimizations prevent needless re-renders, but it's unopinionated about how you optimize expensive computations within your selectors. This is where reselect comes in.
Working with selectors in Zustand and Redux
Now, let's look at a more formal explanation. This article provides a clear example of an expensive computation and introduces reselect as the solution.
Read the section titled 'Performing computations based on state change'. Pay attention to the Fibonacci example, how it's identified as a performance problem, and how the article introduces memoization and reselect as the solution.
3. How reselect Works: createSelector
The primary tool from reselect is the createSelector function. It generates a new, memoized selector.
createSelector takes two or more arguments:
- Input Selectors: An array of simple functions that extract the pieces of state your calculation depends on.
- Result Function: A final function that receives the results of the input selectors as arguments. This is where you perform your expensive computation.
The magic of reselect is that it will only execute the result function if the values returned by the input selectors have changed since the last call. If the inputs are the same, it returns the cached result from the last successful computation.
This video, while based on Redux, gives an excellent visual demonstration of createSelector in action. The principles are identical for Zustand.
Reselect with Redux and React.js
Watch this segment from 'Reselect with Redux and React.js' by aboutscript. It clearly explains the problem of re-computation and demonstrates how to build a basic selector with createSelector.
Watch from 01:17 to 04:30. The first part (until 02:41) recaps the problem of new array references, which will be familiar. The second part (from 02:41) is the key section where the presenter implements createSelector. Focus on the structure: the input selectors and the final result function.
4. Integrating reselect with Zustand
Now, let's translate this into a complete Zustand example. The memoized selector you create is just a function that takes state as an argument, which is exactly what the useStore hook expects. The integration is seamless.
Let's build a memoized selector for our to-do list application.
First, ensure you have reselect installed:npm install reselect
Now, let's define our store and selectors. It's common practice to define selectors in the same file as the store.
import { create } from 'zustand';
import { createSelector } from 'reselect';
// Our basic Zustand store
const useTodoStore = create((set) => ({
todos: [
{ id: 1, text: 'Learn Zustand', completed: true },
{ id: 2, text: 'Integrate Reselect', completed: false },
{ id: 3, text: 'Build an app', completed: false },
{ id: 4, text: 'Master state management', completed: true },
],
// This value can change without affecting the todo list
unrelatedCounter: 0,
incrementCounter: () => set((state) => ({ unrelatedCounter: state.unrelatedCounter + 1 })),
}));
// 1. Input selectors: Simple functions to grab the data we need.
const selectTodos = (state) => state.todos;
// 2. Memoized selector using createSelector
export const selectCompletedTodos = createSelector(
[selectTodos], // Array of input selectors
(todos) => { // The result function
console.log('Computing completed todos...'); // For demonstration
return todos.filter(todo => todo.completed);
}
);
export const selectCompletedTodoCount = createSelector(
[selectCompletedTodos], // We can even use other selectors as input!
(completedTodos) => {
console.log('Computing completed todo count...');
return completedTodos.length;
}
);
Now, let's use these in our components.
// Component to display the list of completed todos
function CompletedTodoList() {
// Use the memoized selector directly in the hook
const completedTodos = useTodoStore(selectCompletedTodos);
return (
<div>
<h3>Completed Todos</h3>
<ul>
{completedTodos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
</div>
);
}
// Component to display the count
function CompletedTodoCounter() {
const count = useTodoStore(selectCompletedTodoCount);
return <h3>Total Completed: {count}</h3>;
}
// Component to change unrelated state
function UnrelatedComponent() {
const increment = useTodoStore(state => state.incrementCounter);
const count = useTodoStore(state => state.unrelatedCounter);
return <button onClick={increment}>Increment Unrelated Counter: {count}</button>
}
Try running this:
When you click the "Increment Unrelated Counter" button, the unrelatedCounter state will change. You will notice that the "Computing..." messages from our selectors do not appear in the console. The expensive filter and length calculations are skipped entirely because their input (state.todos) did not change.
The computation only runs when you modify the todos array itself. We have successfully solved the unnecessary computation problem.
Conclusion
You have now mastered a powerful optimization technique for state management in React. By combining Zustand's efficient re-renders with reselect's memoization, you can build highly performant applications that handle complex, derived data with ease.
Key Takeaways:
- Memoization is a caching technique that prevents expensive functions from re-running with the same inputs.
reselectis the standard library for creating memoized selectors and integrates perfectly with Zustand.- The
createSelectorfunction takes an array of input selectors and a result function. - The result function is only re-executed if the output of an input selector changes, avoiding unnecessary computation.
- You use a memoized selector just like any other selector:
useStore(myMemoizedSelector).
In our next lesson, we will explore a different aspect of Zustand's API: the subscribe function. This will allow us to react to state changes outside of React components, opening up possibilities for tasks like analytics, logging, or syncing with external, non-React libraries.