Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to your next lesson.
In our previous modules, we've built a strong foundation in RxJS, covering everything from Observables and operators to Subjects and their integration with React. Now, we're moving into Module 6, where we'll explore how to combine RxJS with a modern state management library to build powerful, scalable applications.
This lesson marks our first step into that integration by focusing on Zustand. The learning outcome for today is to create and manage a basic Zustand store, covering atomic state updates, actions, and selectors.
We'll start by understanding why a library like Zustand is useful, even in a world with React's built-in hooks. Then, we'll dive into its core API: creating a store, defining state and actions, and connecting it to your React components. By the end of this lesson, you'll have a solid grasp of Zustand's fundamentals, which will be the bedrock for our next lesson where we'll bridge Zustand with RxJS streams.
Given your extensive experience with React, you'll likely appreciate Zustand's minimalist approach and how it contrasts with more boilerplate-heavy libraries you may have used in the past.
1. The "Why": State Management in React
In any non-trivial React application, managing state that needs to be shared across many components is a central challenge. While React provides useState for local state and the Context API for "prop-drilling" avoidance, they both come with trade-offs, particularly around performance and boilerplate.
To understand the problem Zustand solves, please watch the beginning of the following tutorial. It provides a concise overview of the state management landscape in React.
This clip from the 'Zustand - Complete Tutorial' by Cosden Solutions explains the limitations of React's native state management and introduces Zustand as a lightweight solution.
Watch from the beginning until 02:31. Focus on the discussion of why useState and the Context API can be insufficient for complex global state.
As the video mentions, Zustand provides a way to create global state that is not tied to the component tree, avoiding the need for Provider wrappers and the performance issues they can cause.
For developers familiar with Redux, Zustand's architecture is refreshingly simple. It dispenses with concepts like dispatchers, action creators, and reducers in favor of a more direct, hook-based API.
The article "React State Management — using Zustand" provides a clear architectural comparison. The diagrams in the "Zustand vs. Redux" section visually summarize the difference in complexity.
React State Management — using Zustand
This section from a Medium article by Globant provides a high-level architectural comparison between Redux and Zustand.
Read the section titled 'Zustand vs. Redux'. Pay close attention to the two architecture diagrams, which starkly contrast the data flow in Redux versus Zustand.
This simplified model is a key reason for Zustand's growing popularity. It offers the power of a centralized store without the ceremonial overhead.
2. Creating a Zustand Store
At the heart of Zustand is the create function. It takes a single function as an argument and returns a custom hook that your components will use to interact with the store.
Let's see how to define a store. We'll continue with the "Zustand - Complete Tutorial" video, which demonstrates this with TypeScript, aligning with modern development practices.
This segment shows how to use the create function to define a store, including its state shape and initial values.
Watch from 02:31 to 04:09. Notice how a TypeScript type is first defined for the store, and then the create function is used to return the initial state.
As you saw, the process is straightforward:
- Import
create:import { create } from 'zustand' - Define the store's type (in TypeScript): This includes both state properties and the functions (actions) that will modify them.
- Create the store:
export const useMyStore = create<MyStoreType>()(...) - Provide the initializer function: This function returns an object containing the initial state values.
The function passed to create receives a set argument, which is the mechanism for updating state. We'll explore that next.
3. Interacting with the Store: Selectors and Actions
Once the store is created, you need to be able to do two things:
- Read state from it (using Selectors).
- Write state to it (using Actions).
Accessing State with Selectors
The hook returned by create (e.g., useCounterStore) is used inside your components to "select" pieces of state. The key principle is to only select the state your component actually needs. Subscribing to the entire store object can lead to unnecessary re-renders.
This practice is called using atomic selectors.
The following video segments demonstrate how to select state and explain the performance implications.
These clips demonstrate how to use the store hook to select state and emphasize the best practice of using specific, atomic selectors.
First, watch from 04:09 to 05:39 to see how to select a single piece of state. Then, watch from 16:11 to 17:43, which explains why this is critical for performance.
The core idea is that useCounterStore(state => state.count) creates a subscription only to the count property. The component will only re-render if count changes. In contrast, useCounterStore(state => state) would cause a re-render on any state change.
Updating State with Actions
Actions are functions defined within your store that modify the state. They use the set function provided by the create initializer.
The set function can be used in two ways:
set({ count: 1 }): Merges the provided object into the state. This is a "fire-and-forget" update.set(state => ({ count: state.count + 1 })): Use when the new state depends on the previous state. This functional update form is safer as it guarantees you're working with the latest state.
You can also access the current state from within an action using the get() function, which is the second argument to the create initializer. This is useful for complex actions that need to read other state values without subscribing to them.
Let's see how to define and use actions.
This clip covers defining synchronous actions in the store to update state using the set function.
Watch from 05:39 to 09:57. Pay attention to the two ways set is used: first with a hardcoded object, and then with a function to access the previous state.
To summarize the complete create signature:create((set, get) => ({ ... }))
set: The function to update state.get: The function to read the current state inside actions.
4. A Practical Example: To-Do List
Theory is great, but let's see it all come together. The following resource walks through building a complete to-do application. This example effectively demonstrates the concepts we've just covered:
- A store with both data (
todosarray) and actions (addTodo,toggleTodo,deleteTodo). - Components that select state and actions from the store.
- How calling an action in one component correctly triggers a re-render in another.
This section of the 'How to use Zustand' article from refine.dev provides a complete, practical example of a to-do app.
Read the section 'Build a To-do app using Zustand'. Study the useStore implementation and see how the DisplayTodos and TodosControl components interact with it. Note how DisplayTodos selects both state and actions in a single hook call.
5. Accessing the Store Outside Components
A powerful feature of Zustand, especially relevant for our upcoming RxJS integration, is the ability to interact with the store from outside the React component lifecycle. Since the store object itself is not a hook, you can call methods on it directly.
useMyStore.getState(): Returns the current state.useMyStore.setState(): Updates the state, similar to thesetfunction.
This allows for utility functions, services, or middleware to read from and write to your global state without needing to be part of the React tree.
This final clip from the tutorial demonstrates how to use getState and setState to interact with the store from plain JavaScript functions.
Watch from 12:50 to 16:11. This concept will be crucial when we build RxJS middleware in the next lesson, as our Observable logic will live outside our components.
Conclusion
In this lesson, we've established a solid foundation in Zustand. You now have the tools to create and manage centralized state in a React application with a clean and efficient API.
Key Takeaways:
- Simple but Powerful: Zustand provides global state management without the boilerplate of libraries like Redux or the performance pitfalls of the Context API.
- The
createfunction: This is the entry point for creating a store. It returns a custom hook. - State and Actions: A store contains both the state data and the actions (functions) that modify it using the
setandgetfunctions. - Atomic Selectors: For optimal performance, components should select only the specific slices of state they need. This minimizes re-renders.
- Component-Independent: The store's state can be accessed and modified from outside of React components using
getState()andsetState(), making it highly flexible.
Next Lesson Preview:
Now that we understand how Zustand works, we're ready to bring RxJS back into the picture. In the next lesson, we will implement Zustand middleware to bridge actions with Observable streams. This will allow us to handle complex asynchronous side effects, like API requests, in a declarative and robust way, directly addressing your goal of applying RxJS to real-world state management challenges.