{"id":6584,"date":"2025-07-21T14:56:35","date_gmt":"2025-07-21T19:56:35","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6584"},"modified":"2025-07-21T14:56:36","modified_gmt":"2025-07-21T19:56:36","slug":"introducing-zustand","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/introducing-zustand\/","title":{"rendered":"Introducing Zustand (State Management)"},"content":{"rendered":"\n<p><a href=\"https:\/\/github.com\/pmndrs\/zustand\">Zustand<\/a> is a minimal, but fun and effective state management library. It&#8217;s somewhat weird for me to write an introductory blog post on a tool that&#8217;s over 5 years old and pretty popular. But it&#8217;s popular for a reason, and there are almost certainly more developers who aren&#8217;t familiar with it than are. So if you&#8217;re in the former group, hopefully this post will be the concise and impactful introduction you didn&#8217;t know you needed.<\/p>\n\n\n\n<p>The code for everything in this post is on <a href=\"https:\/\/github.com\/arackaf\/zustand-sandbox\">my GitHub repo<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"getting-started\">Getting Started<\/h2>\n\n\n\n<p>We\u2019ll look at a toy&nbsp;task management app&nbsp;that does minimal work so we can focus on state management. It shows a (static) list of tasks, a button to add a new task, a heading showing the number of tasks, and a component to change the UI view between three options.<\/p>\n\n\n\n<p>Moreover, the same app was written 3 times, once using vanilla React context for state, once using Zustand simply but non-idiomatically, and then a third version using Zustand more properly, so we can see some of the performance benefits it offers.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"665\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?resize=1024%2C665&#038;ssl=1\" alt=\"\" class=\"wp-image-6587\" style=\"width:660px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?resize=1024%2C665&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?resize=300%2C195&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?resize=768%2C499&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?resize=1536%2C998&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img1-app.png?w=1776&amp;ssl=1 1776w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n<\/div>\n\n\n<p>Each of the three apps is identical, except for the label above the <strong>Add New Task<\/strong> button.<\/p>\n\n\n\n<p>Each app is broken down more or less identically as so.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">App<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Rendering App\"<\/span>);\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"m-5 p-5 flex flex-col gap-2\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">VanillaLabel<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">AddNewTask<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TasksCount<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TasksHeader<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Filter<\/span> \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TasksBody<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It&#8217;s probably more components than needed, but it&#8217;ll help us inspect render performance.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-state-we-need\">The state we need<\/h3>\n\n\n\n<p>Our state payload for this app will include an array of tasks, a method to update the tasks, the current UI view being displayed, a function to update it, and a current filter, with, of course, a method to update it.<\/p>\n\n\n\n<p>Those values can all be declared as various pieces of state, and then passed down the component tree as needed. This is simple and it works, but the excessive amount of prop passing, often referred to as &#8220;prop drilling,&#8221; can get annoying pretty quickly. There are many ways to avoid this, from state management libraries like Zustand, Redux, and MobX, to the regular old React context.<\/p>\n\n\n\n<p>In this post, we&#8217;ll first explore what this looks like using React context, and then we&#8217;ll examine how Zustand can simplify things while improving performance in the process.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-vanilla-version\">The Vanilla Version<\/h2>\n\n\n\n<p>There&#8217;s a very good argument to be made that React&#8217;s context feature was not designed to be a state management library, but that hasn&#8217;t stopped many devs from trying. To avoid excessive prop drilling while minimizing external dependencies, developers will often store the state required for a specific part of their UI in context and access it lower in the component tree as needed.<\/p>\n\n\n\n<p>Our app has its entire state stored like this, but that&#8217;s just a product of how unrealistically small it is.<\/p>\n\n\n\n<p>Let&#8217;s get started. First, we have to declare our context<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> TasksContext = createContext&lt;TasksState&gt;(<span class=\"hljs-literal\">null<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then we need a <em>component<\/em> that renders a <em>Provider<\/em> for that context, while declaring, and then passing in the actual state<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> TasksProvider = <span class=\"hljs-function\">(<span class=\"hljs-params\">{ <span class=\"hljs-params\">children<\/span> }: { <span class=\"hljs-params\">children<\/span>: <span class=\"hljs-params\">ReactNode<\/span> }<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Rendering TasksProvider\"<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> &#91;tasks, setTasks] = useState&lt;Task&#91;]&gt;(dummyTasks);\n  <span class=\"hljs-keyword\">const<\/span> &#91;currentView, setCurrentView] = useState&lt;TasksView&gt;(<span class=\"hljs-string\">\"list\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> &#91;currentFilter, setCurrentFilter] = useState&lt;<span class=\"hljs-built_in\">string<\/span>&gt;(<span class=\"hljs-string\">\"\"<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> value: TasksState = {\n    tasks,\n    setTasks,\n    currentView,\n    setCurrentView,\n    currentFilter,\n    setCurrentFilter,\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> &lt;TasksContext.Provider value={value}&gt;{children}&lt;<span class=\"hljs-regexp\">\/TasksContext.Provider&gt;;\n};<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The logging <code>console.log(\"Rendering TasksProvider\");<\/code> is present in every component in all versions of this app, so we can inspect re-renders.<\/p>\n\n\n\n<p>Notice how we have to declare each piece of state with <code>useState<\/code> (or <code>useReducer<\/code>)<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> &#91;tasks, setTasks] = useState&lt;Task&#91;]&gt;(dummyTasks);\n<span class=\"hljs-keyword\">const<\/span> &#91;currentView, setCurrentView] = useState&lt;TasksView&gt;(<span class=\"hljs-string\">\"list\"<\/span>);\n<span class=\"hljs-keyword\">const<\/span> &#91;currentFilter, setCurrentFilter] = useState&lt;<span class=\"hljs-built_in\">string<\/span>&gt;(<span class=\"hljs-string\">\"\"<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>and then splice it together in our big state payload, and then render our context provider<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> value: TasksState = {\n  tasks,\n  setTasks,\n  currentView,\n  setCurrentView,\n  currentFilter,\n  setCurrentFilter,\n};\n\n<span class=\"hljs-keyword\">return<\/span> &lt;TasksContext.Provider value={value}&gt;{children}&lt;<span class=\"hljs-regexp\">\/TasksContext.Provider&gt;;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To <em>get<\/em> the current context value in a component that wants to use it, we call the <code>useContext<\/code> hook, and pass in the context object we declared above. To simplify this, it&#8217;s not uncommon to build a simple hook for just this purpose.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> useTasksContext = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> useContext(TasksContext);\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now components can grab whatever slice of state they need.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> { currentView, tasks, currentFilter } = useTasksContext();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"whats-the-problem\">What&#8217;s the problem?<\/h3>\n\n\n\n<p>This code is <em>fine<\/em>. It&#8217;s simple enough. And it works. I&#8217;ll be honest, though, as someone who works with code like this a lot, the boilerplate can become annoying pretty quickly. We have to declare each piece of state with the normal React primitives (useState, useReducer), and then also integrate it into our context payload (and typings). It&#8217;s not the worst thing to deal with; it&#8217;s just annoying.<\/p>\n\n\n\n<p>Another downside of this code is that <em>all<\/em> consumers of this context will always rerender anytime <em>any<\/em> part of the context changes, even if that particular component is not using the part of the context that just changed. We can see that with the logging that&#8217;s in these components.<\/p>\n\n\n\n<p>For example, changing the current UI view rerenders everything, even though only the task header, and task body read that state<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"544\" height=\"510\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img2-context-rerender.png?resize=544%2C510&#038;ssl=1\" alt=\"\" class=\"wp-image-6589\" style=\"width:370px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img2-context-rerender.png?w=544&amp;ssl=1 544w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img2-context-rerender.png?resize=300%2C281&amp;ssl=1 300w\" sizes=\"auto, (max-width: 544px) 100vw, 544px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"introducing-zustand\">Introducing Zustand<\/h2>\n\n\n\n<p>Zustand is a minimal but powerful state management library. To create state, Zustand gives you a <code>create<\/code> method<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { create } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"zustand\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It&#8217;s easier to show this than to describe it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> useTasksStore = create&lt;TasksState&gt;(<span class=\"hljs-function\"><span class=\"hljs-params\">set<\/span> =&gt;<\/span> ({\n  tasks,\n  setTasks: <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">arg<\/span>: <span class=\"hljs-params\">Task<\/span>&#91;] | ((<span class=\"hljs-params\">tasks<\/span>: <span class=\"hljs-params\">Task<\/span>&#91;]<\/span>) =&gt;<\/span> Task&#91;])) =&gt; {\n    <span class=\"hljs-keyword\">set<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> {\n      <span class=\"hljs-keyword\">return<\/span> {\n        tasks: <span class=\"hljs-keyword\">typeof<\/span> arg === <span class=\"hljs-string\">\"function\"<\/span> ? arg(state.tasks) : arg,\n      };\n    });\n  },\n  currentView: <span class=\"hljs-string\">\"list\"<\/span>,\n  setCurrentView: <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">newView<\/span>: <span class=\"hljs-params\">TasksView<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-keyword\">set<\/span>({ currentView: newView }),\n  currentFilter: <span class=\"hljs-string\">\"\"<\/span>,\n  setCurrentFilter: <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">newFilter<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-keyword\">set<\/span>({ currentFilter: newFilter }),\n}));<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We pass a function to <code>create<\/code> and return our state. Just like that. Simple and humble. The function we pass also takes an argument, which I&#8217;ve called <code>set<\/code>. The result of the <code>create<\/code> function, which I&#8217;ve named <code>useTasksStore<\/code> here, will be a React hook that you use to <em>read<\/em> your state.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"updating-state\">Updating state<\/h3>\n\n\n\n<p>Updating our state couldn&#8217;t be simpler. The <code>set<\/code> function we see above is how we do that. Notice our updating functions like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">setCurrentView: <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">newView<\/span>: <span class=\"hljs-params\">TasksView<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-keyword\">set<\/span>({ currentView: newView }),<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>By default <code>set<\/code> will take what we return, and <em>integrate it<\/em> into the state that&#8217;s already there. So we can return the pieces that have changed, and Zustand will handle the update.<\/p>\n\n\n\n<p>Naturally, there&#8217;s an override: if we pass <code>true<\/code> for the second argument to <code>set<\/code>, then what we return will overwrite the existing state in its entirety. <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">clear: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> <span class=\"hljs-keyword\">set<\/span>({}, <span class=\"hljs-literal\">true<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The above would wipe our state, and replace it with an empty object; use this cautiously!<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"reading-our-state\">Reading our state<\/h3>\n\n\n\n<p>To read our state in the components which need it, we call the hook that was returned from <code>create<\/code>, which would be <code>useTasksStore<\/code> from above. We <em>could<\/em> read our state in the same way we read our context above<\/p>\n\n\n\n<p class=\"learn-more\">This is not the best way to use Zustand. Keep reading for a better way to use this API.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> { currentView, tasks, currentFilter } = useTasksStore();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This will work and behave exactly like our context example before.<\/p>\n\n\n\n<p>This means changing the current UI view will again re-render all components that read <em>anything<\/em> from the Zustand store, whether related to this piece of state, or not.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"558\" height=\"460\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img3-zustand-default-rerender.png?resize=558%2C460&#038;ssl=1\" alt=\"\" class=\"wp-image-6590\" style=\"width:386px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img3-zustand-default-rerender.png?w=558&amp;ssl=1 558w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img3-zustand-default-rerender.png?resize=300%2C247&amp;ssl=1 300w\" sizes=\"auto, (max-width: 558px) 100vw, 558px\" \/><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"the-correct-way-to-read-state\">The Correct Way to Read State<\/h2>\n\n\n\n<p>It&#8217;s easy to miss in the docs the first time you read them, but when reading from your Zustand store, you shouldn&#8217;t do this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> { yourFields } = useTasksStore();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Zustand is well optimized, and will cause the component with the call to <code>useTasksStore<\/code> to only re-render when the <em>result<\/em> of the hook call changes. By default, it returns an object with your entire state. And when you change any piece of your state, the surrounding object will have to be recreated by Zustand, and will no longer match.<\/p>\n\n\n\n<p>Instead, you should pass a selector argument into <code>useTasksStore<\/code>, in order to <em>select<\/em> the piece of state you want. The simplest usage would look like this<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> currentView = useTasksStore(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> state.currentView);\n<span class=\"hljs-keyword\">const<\/span> tasks = useTasksStore(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> state.tasks);\n<span class=\"hljs-keyword\">const<\/span> currentFilter = useTasksStore(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> state.currentFilter);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now our call returns only the <code>currentView<\/code> value in the first line, or our <code>tasks<\/code> array, or <code>currentFilter<\/code> in our second and third lines, respectively.<\/p>\n\n\n\n<p>The value returned for <code>currentView<\/code> will only be different if you&#8217;ve <em>changed<\/em> that state value, and so on with <code>tasks<\/code>, and <code>currentFilter<\/code>. That means if <em>none<\/em> of these values have changed, then this component will not rerender, even if <em>other<\/em> values in our Zustand store have changed.<\/p>\n\n\n\n<p>If you don&#8217;t like having those multiple calls, you&#8217;re free to use Zustand&#8217;s <code>useShallow<\/code> helper<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { useShallow } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"zustand\/react\/shallow\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n<span class=\"hljs-keyword\">const<\/span> { tasks, setTasks } = useTasksStore(\n  useShallow(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> ({\n    <span class=\"hljs-attr\">tasks<\/span>: state.tasks,\n    <span class=\"hljs-attr\">setTasks<\/span>: state.setTasks,\n  }))\n);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>useShallow<\/code> hook lets us return an object with the state we want, and will trigger a rerender only if a shallow check on the properties in this object change.<\/p>\n\n\n\n<p>If you want to save a few lines of code, you&#8217;re also free to return an array with <code>useShallow<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> &#91;tasks, setTasks] = useTasksStore(useShallow(<span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> &#91;state.tasks, state.setTasks]));<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This does the same thing.<\/p>\n\n\n\n<p>The Zustand-optimized version of the app only uses the <code>useTasksStore<\/code> hook with a selector function, which means we can observe our improved re-rendering.<\/p>\n\n\n\n<p>Changing the current UI view will only rerender the components that use the ui view part of the state.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"558\" height=\"296\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img4-zustand-optimized.png?resize=558%2C296&#038;ssl=1\" alt=\"Console log showing rendering messages for TasksHeader, TasksBody, and TasksDetailed components.\" class=\"wp-image-6591\" style=\"width:386px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img4-zustand-optimized.png?w=558&amp;ssl=1 558w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/img4-zustand-optimized.png?resize=300%2C159&amp;ssl=1 300w\" sizes=\"auto, (max-width: 558px) 100vw, 558px\" \/><\/figure>\n<\/div>\n\n\n<p>For a trivial app like this, it probably won&#8217;t matter, but for a large app at scale, this can be beneficial, especially for users on slower devices.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"odds-and-ends\">Odds &amp; Ends<\/h2>\n\n\n\n<p>The full <a href=\"https:\/\/github.com\/pmndrs\/zustand\">Zustand docs are here<\/a>. Zustand has a delightfully small surface area, so I&#8217;d urge you to just read the docs if you&#8217;re curious.<\/p>\n\n\n\n<p>That being said, there are a few features worth noting here.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"async-friendly\">Async friendly<\/h3>\n\n\n\n<p>Zustand doesn&#8217;t care where or when the <code>set<\/code> function is called. You&#8217;re free to have async methods in your store, which call <code>set<\/code> after a fetch.<\/p>\n\n\n\n<p>The docs offer this example:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> useFishStore = create(<span class=\"hljs-function\"><span class=\"hljs-params\">set<\/span> =&gt;<\/span> ({\n  <span class=\"hljs-attr\">fishies<\/span>: {},\n  <span class=\"hljs-attr\">fetch<\/span>: <span class=\"hljs-keyword\">async<\/span> pond =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(pond);\n    <span class=\"hljs-keyword\">set<\/span>({ fishies: <span class=\"hljs-keyword\">await<\/span> response.json() });\n  },\n}));<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"reading-state-inside-your-store-but-outside-of-set\">Reading state inside your store, but outside of <code>set<\/code><\/h3>\n\n\n\n<p>We already know that we can call <code>set(oldState =&gt; newState)<\/code>, but what if we need (or just want) to read the <em>current<\/em> state inside one of our actions, unrelated to an update?<\/p>\n\n\n\n<p>It turns out <code>create<\/code> also has a second argument, <code>get<\/code>, that you can use for this very purpose<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> useTasksStore = create&lt;TasksState&gt;<span class=\"hljs-function\">(<span class=\"hljs-params\">(<span class=\"hljs-params\">set<\/span>, <span class=\"hljs-params\">get<\/span><\/span>) =&gt;<\/span> ({<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And now you can do something like this<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">logOddTasks: <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> oddTasks = <span class=\"hljs-keyword\">get<\/span>().tasks.filter((_, index) =&gt; index % 2 === 0);\n  console.log({ oddTasks: oddTasks });\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The first line grabs a piece of state, completely detached from any updates.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"reading-state-outside-of-react-components\">Reading state outside of React components<\/h3>\n\n\n\n<p>Zustand gives you back a React <em>hook<\/em> from <code>create<\/code>. But what if you want to read your state outside of a React component? Zustand attaches a <code>getState()<\/code> method directly onto your hook, which you can call anywhere.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  setTimeout(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Can't call a hook here\"<\/span>);\n    <span class=\"hljs-keyword\">const<\/span> tasks = useTasksStore.getState().tasks;\n    <span class=\"hljs-built_in\">console<\/span>.log({ tasks });\n  }, <span class=\"hljs-number\">1000<\/span>);\n}, &#91;]);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"pushing-further\">Pushing further<\/h3>\n\n\n\n<p>Zustand also supports manual, fine-grained subscriptions; bindings for vanilla JavaScript, with no React at all; and integrates well with immutable helpers like Immer. It also has some other, more advanced goodies that we won&#8217;t try to cover here. Check out the docs if this post has sparked your interest!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"concluding-thoughts\">Concluding Thoughts<\/h2>\n\n\n\n<p>Zustand is a wonderfully simple, frankly fun library to use to manage state management in React. And as an added bonus, it can also improve your render performance.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Zustand is a minimal, but fun and effective state management library which just may improve your render performance.<\/p>\n","protected":false},"author":21,"featured_media":6605,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[3,62,374,373],"class_list":["post-6584","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-javascript","tag-react","tag-state-management","tag-zustand"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/07\/Introducing-Zustand.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6584","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6584"}],"version-history":[{"count":14,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6584\/revisions"}],"predecessor-version":[{"id":6610,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6584\/revisions\/6610"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6605"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6584"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6584"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6584"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}