React 19.2.8 and Vue 3.5.41 both shipped patch updates in the last month, and the state management layer around each framework has quietly reshuffled. Redux still leads GitHub stars in the React ecosystem, but most new projects skip it for Zustand. Vuex is effectively retired, and Pinia is now the only state library the Vue core team recommends. If you’re starting a project today, or migrating one, picking the wrong store pattern costs weeks of refactoring later. This tutorial builds the same small app twice, once with React 19.2 (Redux Toolkit and Zustand) and once with Vue 3.5.41 (Pinia), so you can see the real code, not just the theory, and decide which pattern fits your team.
By the end you’ll have two working counter-and-cart apps wired to global state, a working async data-fetch flow in each store, a test suite for both, and a troubleshooting list built from the errors developers actually hit when setting up react state management or Pinia for the first time. Total build time: roughly 100 minutes if you follow every step, less if you only need one framework’s setup.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Prerequisites: What You Need Before Starting
Confirm these versions before you begin. Mismatched versions are the single biggest cause of broken store setups reported in GitHub issues for both ecosystems.
| Tool | Minimum Version | Recommended for This Tutorial | Check Command |
|---|---|---|---|
| Node.js | 20.19 LTS | 22 LTS or 24 LTS | node -v |
| npm | 10.x | Latest bundled with Node 22/24 | npm -v |
| React | 18.x | 19.2.8 (current stable, patched Jul 21, 2026) | npm ls react |
| Vue | 3.4.x | 3.5.41 (current stable, patched Aug 5, 2026) | npm ls vue |
| Redux Toolkit | 2.x | Latest 2.x release | npm ls @reduxjs/toolkit |
| Zustand | 4.x | Latest stable release | npm ls zustand |
| Pinia | 2.x | Latest stable release | npm ls pinia |
| Vite | 5.x | Latest stable release | npx vite –version |
| TypeScript | 5.x | Latest stable release | npx tsc -v |
| Vitest | 2.x | Latest stable release | npx vitest –version |
You’ll also need a code editor (VS Code with the Vue Language Features extension and the ESLint extension is the smoothest combo), a terminal, and roughly 500MB of free disk space for two separate node_modules folders. No paid accounts, API keys, or cloud services are required for anything in this guide.
What Changed in React 19.2.8 and Vue 3.5.41
Before writing any store code, it’s worth knowing what you’re actually building on. React 19 introduced the Actions API, the use() hook for reading promises and context conditionally, and built-in support for form actions without extra libraries; the 19.2.8 patch released July 21, 2026 is a stability release on top of that baseline, not a feature drop, so everything in this tutorial targets the same APIs your team is likely already running. None of the store patterns here depend on the newest React 19 features directly, but the use() hook is worth knowing about if you later want to read a Zustand-backed promise straight inside a component render.
On the Vue side, Vue 3.5 shipped reactivity improvements that reduced memory overhead for large reactive objects and introduced experimental Vapor Mode, a compiler strategy that skips the Virtual DOM entirely for eligible components. The 3.5.41 patch released August 5, 2026 rolled in bug fixes to the reactivity transform and template ref handling; it doesn’t change any Pinia API surface, so the store code in this tutorial runs unmodified whether you’re on 3.5.0 or 3.5.41. If your project still targets Vue 3.4, every Pinia example here still works without changes, since Pinia’s Composition API store syntax has been stable since Vue 3’s initial 3.x releases.
Step 1: Understand the State Management Paradigms Before You Pick One
React ships with no built-in global store beyond Context and useReducer, which is why the ecosystem sprawled into Redux, Zustand, Jotai, Recoil, MobX, and Valtio. Redux remains the most widely adopted state management library in the React ecosystem by GitHub star count, with roughly 61,000 stars, largely because of a decade of enterprise adoption and mature middleware. But for new projects in 2026, most teams reach for Zustand first: it needs no Provider wrapper, no action-type constants, and no reducer boilerplate. If you’re searching for “react state management” tutorials right now, you’ll notice most current guides steer beginners toward Zustand and save Redux Toolkit for teams that already have it in production.
Every library covered in this guide solves the same underlying problem: sharing data across components that aren’t directly related in the tree, without prop-drilling it through five layers of intermediate components. What differs is how much structure the library imposes on you to solve that problem, and how much of that structure you actually need at your app’s current size.
Vue took the opposite path. Vuex was the official store for years, but the Vue core team now recommends Pinia for every new Vue 3 project, and Vuex is in maintenance mode only. Pinia has around 14,600 GitHub stars and plugs directly into Vue’s reactivity system, meaning you mutate state directly inside actions instead of dispatching plain objects through reducers. That single design choice is the biggest day-to-day difference between the React and Vue approaches covered in this tutorial.
“A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks and isn’t boilerplatey or opinionated.”
Zustand Documentation — zustand.docs.pmnd.rs
Keep this mental model as you follow the steps below: Redux Toolkit gives you structure and traceability at the cost of setup time; Zustand gives you speed at the cost of built-in conventions; Pinia gives Vue developers both, because it’s the framework’s own opinionated answer rather than a third-party import.
Step 2: Scaffold the React 19.2 Project With Vite
Open a terminal and create a new Vite-powered React project with TypeScript. This gets you React 19.2.8 with fast refresh and no bundler configuration required.
npm create vite@latest react-state-demo -- --template react-ts
cd react-state-demo
npm install
npm run dev
Confirm the dev server is running on http://localhost:5173 and that npm ls react reports version 19.x. If it reports 18.x, your global npm cache is serving a stale template; clear it with npm cache clean --force and re-run the scaffold command. The scaffold uses Vite under the hood rather than Create React App, which has been unmaintained for years; if you’re following an older tutorial that still recommends CRA, skip it entirely.
Step 3: Scaffold the Vue 3.5.41 Project With Vite
In a separate folder, scaffold the Vue equivalent. Vue’s official create tool now defaults to the Composition API with TypeScript enabled by prompt.
npm create vue@latest vue-state-demo
# When prompted, select: TypeScript = Yes, Pinia = Yes, Vitest = Yes
cd vue-state-demo
npm install
npm run dev
Selecting Pinia at scaffold time saves you a manual install step later, but this tutorial walks through the manual install anyway so you understand what the CLI wires up for you. Verify with npm ls vue that you’re on the 3.5.x line before continuing.
Step 4: Install and Configure Redux Toolkit in React
Redux Toolkit (RTK) is the officially recommended way to write Redux in 2026; nobody should hand-write raw Redux boilerplate anymore. Install both the toolkit and the React bindings.
npm install @reduxjs/toolkit react-redux
Create src/store/cartSlice.ts:
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
status: 'idle' | 'loading' | 'error'
}
const initialState: CartState = { items: [], status: 'idle' }
const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction) {
const existing = state.items.find(i => i.id === action.payload.id)
if (existing) {
existing.quantity += 1
} else {
state.items.push(action.payload)
}
},
removeItem(state, action: PayloadAction) {
state.items = state.items.filter(i => i.id !== action.payload)
},
},
})
export const { addItem, removeItem } = cartSlice.actions
export default cartSlice.reducer
Then wire it into a store in src/store/index.ts:
import { configureStore } from '@reduxjs/toolkit'
import cartReducer from './cartSlice'
export const store = configureStore({
reducer: { cart: cartReducer },
})
export type RootState = ReturnType
export type AppDispatch = typeof store.dispatch
Wrap your app root with the Redux Provider in main.tsx, passing the store you just created. Forgetting this Provider wrapper is the single most common Redux setup error, and it produces a cryptic “could not find react-redux context value” error with no line number pointing at the missing wrapper.
Step 5: Install and Configure Zustand as the Lightweight Alternative
Now build the same cart with Zustand, so you can compare line counts directly. Install it in the same project (or a fresh one, since the two don’t conflict).
npm install zustand
Create src/store/useCartStore.ts:
import { create } from 'zustand'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
}
export const useCartStore = create((set) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id)
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
}
}
return { items: [...state.items, item] }
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
}))
No Provider, no dispatch, no action types. Any component calls useCartStore() directly and gets the state and the setters in one hook. That’s roughly 30 lines versus the 40+ lines split across two files for the Redux Toolkit version, and it’s the main reason most greenfield React apps in 2026 default to Zustand unless the team already has Redux DevTools workflows they don’t want to lose.
Step 6: Install and Configure Pinia in Vue 3.5.41
If you didn’t select Pinia during scaffolding, install it manually.
npm install pinia
Register it in src/main.ts:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
This single app.use(createPinia()) call replaces the entire Provider-wrapping step React developers need for Redux. Pinia doesn’t require wrapping your component tree in JSX; it registers globally at the app instance level once, and every component gets access to every store from that point on.
Step 7: Build Your First Pinia Store
Create src/stores/cart.ts using the Composition API store syntax, which is the pattern the Vue core team recommends for new projects because it mirrors setup() function syntax you already use in components.
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface CartItem {
id: string
name: string
price: number
quantity: number
}
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
)
function addItem(item: CartItem) {
const existing = items.value.find((i) => i.id === item.id)
if (existing) {
existing.quantity += 1
} else {
items.value.push(item)
}
}
function removeItem(id: string) {
items.value = items.value.filter((i) => i.id !== id)
}
return { items, total, addItem, removeItem }
})
Notice existing.quantity += 1: Pinia lets you mutate state directly because it’s built on Vue’s native reactivity (ref and reactive), unlike Redux where direct mutation outside a reducer breaks the entire update cycle. This is the clearest illustration of the philosophical split between the two ecosystems covered in this tutorial.
Step 8: Connect Components to State in Each Framework
In React with Redux Toolkit, a component reads state with useSelector and writes with useDispatch:
import { useSelector, useDispatch } from 'react-redux'
import type { RootState } from '../store'
import { addItem } from '../store/cartSlice'
function CartSummary() {
const items = useSelector((state: RootState) => state.cart.items)
const dispatch = useDispatch()
return (
<div>
<p>Items in cart: {items.length}</p>
<button onClick={() => dispatch(addItem({ id: '1', name: 'Widget', price: 9.99, quantity: 1 }))}>
Add Widget
</button>
</div>
)
}
In React with Zustand, the same component skips the selector/dispatch split entirely:
import { useCartStore } from '../store/useCartStore'
function CartSummary() {
const items = useCartStore((state) => state.items)
const addItem = useCartStore((state) => state.addItem)
return (
<div>
<p>Items in cart: {items.length}</p>
<button onClick={() => addItem({ id: '1', name: 'Widget', price: 9.99, quantity: 1 })}>
Add Widget
</button>
</div>
)
}
In Vue with Pinia, a component just calls the store function and destructures with storeToRefs to keep reactivity intact:
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart'
const cartStore = useCartStore()
const { items } = storeToRefs(cartStore)
</script>
<template>
<div>
<p>Items in cart: {{ items.length }}</p>
<button @click="cartStore.addItem({ id: '1', name: 'Widget', price: 9.99, quantity: 1 })">
Add Widget
</button>
</div>
</template>
The storeToRefs call matters: destructuring a Pinia store directly without it strips reactivity from primitives, which is the Vue equivalent of the “why isn’t my component re-rendering” bug React developers hit when they forget a dependency array.
Step 9: Handle Async Actions and API Calls
Real apps fetch data. Redux Toolkit ships createAsyncThunk for this; add it to your cart slice:
import { createAsyncThunk } from '@reduxjs/toolkit'
export const fetchCart = createAsyncThunk('cart/fetch', async (userId: string) => {
const res = await fetch(`/api/cart/${userId}`)
if (!res.ok) throw new Error('Failed to fetch cart')
return res.json()
})
// inside createSlice, add an extraReducers block:
// extraReducers: (builder) => {
// builder
// .addCase(fetchCart.pending, (state) => { state.status = 'loading' })
// .addCase(fetchCart.fulfilled, (state, action) => {
// state.items = action.payload
// state.status = 'idle'
// })
// .addCase(fetchCart.rejected, (state) => { state.status = 'error' })
// }
Zustand handles async with a plain async function inside the store, no separate thunk API needed:
fetchCart: async (userId: string) => {
set({ status: 'loading' })
try {
const res = await fetch(`/api/cart/${userId}`)
const items = await res.json()
set({ items, status: 'idle' })
} catch {
set({ status: 'error' })
}
}
Pinia actions are also just async functions, since a Pinia action is nothing more than a method on the store returned from defineStore:
async function fetchCart(userId: string) {
status.value = 'loading'
try {
const res = await fetch(`/api/cart/${userId}`)
items.value = await res.json()
status.value = 'idle'
} catch {
status.value = 'error'
}
}
// remember to return fetchCart from the store setup function
Both Zustand and Pinia let you write async logic the same way you’d write it anywhere else in JavaScript. Redux Toolkit’s thunk pattern adds ceremony, but it also gives you three distinct action states (pending/fulfilled/rejected) automatically dispatched to DevTools, which larger teams rely on for debugging race conditions in production.
Step 10: Write Tests for Both Stores With Vitest
Vitest works for both projects since Vite powers both scaffolds. Install it if it wasn’t added at scaffold time.
npm install -D vitest @testing-library/react @testing-library/vue
A Zustand store test is just a function call, no rendering required:
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from '../store/useCartStore'
describe('cart store', () => {
beforeEach(() => {
useCartStore.setState({ items: [] })
})
it('adds a new item', () => {
useCartStore.getState().addItem({ id: '1', name: 'Widget', price: 9.99, quantity: 1 })
expect(useCartStore.getState().items).toHaveLength(1)
})
})
A Pinia store test needs a fresh Pinia instance per test, using setActivePinia:
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from '../stores/cart'
describe('cart store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('adds a new item', () => {
const store = useCartStore()
store.addItem({ id: '1', name: 'Widget', price: 9.99, quantity: 1 })
expect(store.items).toHaveLength(1)
})
})
Run npx vitest run in each project. Both should report passing tests with no DOM rendering needed, since you’re testing the store logic in isolation from any component.
Step 11: Wire Up DevTools for Debugging
Redux DevTools work out of the box with configureStore, no extra config needed; install the Redux DevTools browser extension and it auto-connects. Zustand needs the DevTools middleware explicitly:
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
export const useCartStore = create()(
devtools((set) => ({
// ...same store body as Step 5
}))
)
Pinia has its own dedicated devtools panel inside Vue DevTools, showing every store’s state tree and a timeline of mutations, and it requires zero extra configuration if you used the standard createPinia() setup from Step 6.
Step 12: Decide Which Pattern Fits Your Project
Use this decision table as your final checklist before committing to a state library for a new build.
| Scenario | Recommended Pick | Why |
|---|---|---|
| New React app, small-to-medium team | Zustand | Minimal boilerplate, no Provider, hook-based API |
| Existing large React codebase already on Redux | Stay on Redux Toolkit | Migration cost outweighs the boilerplate savings |
| React app needing time-travel debugging / strict audit trail | Redux Toolkit | Mature DevTools, predictable action log |
| Any new Vue 3 app | Pinia | Official Vue core team recommendation, replaces Vuex |
| Legacy Vue 2 / Vuex app | Migrate to Pinia incrementally | Vuex is in maintenance mode only |
| Cross-framework team comparing patterns | Zustand (React) + Pinia (Vue) | Closest conceptual match; both are lightweight, hook/composable-based |
“Centralizing your application’s state and logic enables powerful capabilities like undo/redo, state persistence, and much more.”
Redux Documentation — redux.js.org
Performance and Bundle Size Comparison
Bundle size and reactivity model differ meaningfully across the three libraries. Vue 3.5’s reactivity system, when paired with Pinia, benefits from the same optimizations that make Vue’s core rendering fast: benchmarks published in mid-2026 measured Vue 3.5 with Vapor Mode completing a 1,000-row DOM update in about 27 milliseconds, roughly 36% faster than the same test on React 19.2 at approximately 42 milliseconds. That reactivity advantage carries through into Pinia’s direct-mutation model, since there’s no synthetic diffing layer between an action and the DOM update.
| Library | Approx. Setup Lines (this tutorial’s cart) | Requires Provider/App Registration | DevTools |
|---|---|---|---|
| Redux Toolkit | ~55 (slice + store + Provider) | Yes — Provider wrapper | Built-in, zero config |
| Zustand | ~30 (single file) | No | Requires devtools middleware |
| Pinia | ~35 (store + one-line app.use) | Yes — one-line app.use(createPinia()) | Built-in via Vue DevTools |
These line counts come directly from the code you wrote in Steps 4 through 7 of this tutorial, not from a synthetic benchmark, so you can verify them against your own project.
5 Common Pitfalls When Setting Up React or Vue State Management
- Forgetting the Redux Provider wrapper. Every component under an unwrapped tree throws a context error the moment it calls
useSelector. Wrap your root component, not a nested layout, or nested routes will still fail. - Destructuring a Pinia store without storeToRefs. Plain destructuring (
const { items } = useCartStore()) strips reactivity from refs, so your template silently stops updating. Always usestoreToRefsfor state, but call actions directly off the store instance. - Mutating Redux state outside a reducer. Because Redux Toolkit uses Immer under the hood, mutation only works inside a slice’s reducer function. Mutating state in a component or thunk body corrupts the store silently instead of throwing.
- Skipping the Zustand devtools middleware, then wondering why nothing shows in the extension. Redux DevTools connects automatically via
configureStore; Zustand does not, and needs the explicitdevtools()wrapper from Step 11. - Installing Pinia without registering it with app.use(). If you import
defineStoreand calluseCartStore()in a component beforeapp.use(createPinia())runs, you’ll get a runtime error that “getActivePinia() was called but there was no active Pinia.”
Expected Output: What Success Looks Like
After completing Steps 2 through 9, running each dev server and clicking “Add Widget” should produce this console output pattern in Redux DevTools (React + Redux Toolkit build):
action: cart/addItem
payload: { id: '1', name: 'Widget', price: 9.99, quantity: 1 }
state.cart.items: [{ id: '1', name: 'Widget', price: 9.99, quantity: 1 }]
In the Vue + Pinia build, Vue DevTools’ Pinia tab should show the store name “cart” with a live-updating items array and a mutation entry timestamped at the moment you clicked. In the Zustand build with devtools middleware attached, Redux DevTools (yes, Zustand’s devtools plugin reuses the same extension) shows an action labeled with your set-call site.
Troubleshooting: 8 Issues You’ll Likely Hit
- “Could not find react-redux context value” — The Provider isn’t wrapping the component tree, or you have two copies of react-redux installed (check with
npm ls react-reduxfor duplicates in nested node_modules). - “getActivePinia() was called but there was no active Pinia” —
app.use(createPinia())runs after a component tries to use a store, or you’re callinguseCartStore()at module scope instead of insidesetup(). - Zustand state updates but the component doesn’t re-render — You’re selecting the whole store object instead of a specific slice (
useCartStore(state => state)), which can break reference-equality checks in some usage patterns; select only what you need. - Pinia template shows stale data after an API call — You destructured state without
storeToRefsin Step 8; fix the destructure, don’t add a manual watcher as a workaround. - Redux Toolkit TypeScript error “Property does not exist on RootState” — Your reducer wasn’t added to the
configureStorereducer map, so the inferred RootState type doesn’t include that slice. - Zustand devtools tab is blank in the browser extension — The
devtools()middleware needs to be the outermost wrapper around your store creator function; check middleware order if you’re combining it withpersist. - Pinia action throws “Cannot read properties of undefined” on a computed value — You returned the computed ref but forgot to include it in the store’s return object at the bottom of
defineStore‘s setup function. - Vite dev server shows a blank white screen after adding Redux — Usually a missing default export from a slice file; confirm
export default cartSlice.reducerexists and matches the import in your store index file.
Advanced Tips for Production Apps
Once your basic store works, a few upgrades matter before shipping to production. For Redux Toolkit apps fetching from a real backend, replace manual thunks with RTK Query, which handles caching, request deduplication, and automatic refetching without extra state slices. For Zustand, add the persist middleware to sync cart or auth state to localStorage across page reloads, chaining it with devtools in the correct order (persist should wrap the innermost creator, devtools the outermost).
For Pinia, split large stores by domain (cart, auth, catalog) rather than one giant store, since Pinia’s per-store devtools panel makes debugging much easier when stores stay focused. Pinia also supports plugins; the official pinia-plugin-persistedstate package handles localStorage sync with a single line of config, similar to Zustand’s built-in persist middleware.
If your team is migrating an existing Redux codebase toward Zustand incrementally, keep both stores running side by side during the transition instead of a big-bang rewrite. Convert one feature slice at a time, starting with the simplest reducer in your codebase, and remove the Redux Provider only after the last slice migrates.
Complete Working Project: Folder Structure Recap
By the end of this tutorial, your React project should look like this:
react-state-demo/
├── src/
│ ├── store/
│ │ ├── index.ts # configureStore setup
│ │ ├── cartSlice.ts # Redux Toolkit slice
│ │ └── useCartStore.ts # Zustand store
│ ├── components/
│ │ └── CartSummary.tsx
│ ├── App.tsx
│ └── main.tsx # Provider wraps App here
├── package.json
└── vite.config.ts
And your Vue project should look like this:
vue-state-demo/
├── src/
│ ├── stores/
│ │ └── cart.ts # Pinia store (Composition API syntax)
│ ├── components/
│ │ └── CartSummary.vue
│ ├── App.vue
│ └── main.ts # app.use(createPinia()) here
├── package.json
└── vite.config.ts
Both folder structures are intentionally close to each other, which makes it easy to keep both projects open side by side and compare a single feature’s implementation across frameworks as you build.
React State Management Libraries Beyond Redux and Zustand
This tutorial focused on Redux Toolkit and Zustand because they cover the two dominant philosophies (structured vs. minimal) in the React ecosystem, but they aren’t the only options. Jotai offers atomic state management, useful when different pieces of UI need independent, granular state rather than one shared store. Recoil, built by Meta, offers a similar atom-based model but has seen slower community momentum than Jotai in recent years. MobX uses observable objects and works well for teams coming from an object-oriented background. Valtio wraps plain JavaScript objects in a proxy, giving you Vue-like direct mutation inside React, which is worth trying if Pinia’s ergonomics in this tutorial appealed to you more than Redux’s. The full source for the Zustand pattern used throughout this tutorial lives in the Zustand GitHub repository, including additional middleware examples for persistence and immer-style updates that go beyond what’s covered here.
None of these require a rewrite of your whole app to test. Since every option in this article exposes state through a hook, you can install a second library, build one new feature with it, and compare the developer experience against your existing store before committing to a wider migration. A practical way to run this comparison without risking your main branch: create a feature flag that swaps the data source for a single non-critical panel, wire that panel to the new library, and let it run in production for a sprint before deciding whether to expand the pattern further.
Choosing Between Context API and a Dedicated Store Library
A question that comes up in every team discussion about react state management: why not just use Context and skip the extra dependency entirely? Context works fine for state that changes rarely, like theme or authenticated user info, because every consumer re-renders on any change to the context value. That’s fine for a handful of updates a session; it becomes a performance problem once you put frequently changing state (like a shopping cart quantity or a live search filter) into a single Context provider, since every subscribed component re-renders on every keystroke.
Zustand and Redux Toolkit solve this with selector-based subscriptions: a component only re-renders when the specific slice of state it selected actually changes, not on every store update. Pinia gets the same benefit for free from Vue’s underlying reactivity system, since Vue components only re-render when a reactive value they actually read in their template changes, regardless of which store that value came from. If your app’s shared state changes more than a few times per user session, reach for one of the three patterns in this tutorial rather than Context or a single global reactive() object.
Vue State Management: Why Vuex Isn’t the Default Anymore
If you’re maintaining an older Vue 2 or early Vue 3 codebase, you may still see Vuex in the dependency tree. Vuex is not deprecated in the sense of being removed, but it is in maintenance mode, meaning it receives security patches, not new features. The Vue core team has pointed new projects toward Pinia since it became the officially recommended solution, largely because Pinia has full TypeScript inference without extra type-casting helpers, no nested modules namespace syntax to memorize, and roughly 40-60% less boilerplate for an equivalent store, based on side-by-side store comparisons published by Vue ecosystem writers in 2026.
Migrating from Vuex to Pinia is usually done store-by-store rather than all at once. Convert your smallest, least-coupled Vuex module first, verify it behaves identically in Vue DevTools, then work outward toward more complex modules with cross-module getters, which need the most careful refactoring since Pinia stores reference each other directly rather than through a shared root state tree. The Pinia GitHub repository includes an official Vuex-to-Pinia migration guide in its docs folder if you want a checklist beyond what’s covered here.
Frequently Asked Questions
Is Redux still worth learning in 2026?
Yes, particularly for enterprise React roles. Redux remains the most-starred state management library in the React ecosystem, and large codebases built over the past decade still run on it. Job listings for senior React roles frequently list Redux experience as a requirement even when the team is evaluating Zustand for new features.
Can I use Zustand and Redux Toolkit in the same React app?
Technically yes, and it’s a common migration pattern: run both side by side while you convert Redux slices to Zustand stores one feature at a time. It’s not recommended as a permanent architecture, since having two sources of global state truth increases the chance of stale-state bugs.
Does Pinia replace Vuex completely?
For new projects, yes. Pinia is the Vue core team’s official recommendation for Vue 3 state management, and it covers everything Vuex did with less boilerplate and full TypeScript inference. Vuex still works and receives maintenance patches, but no new Vuex features are planned.
Which is faster: React with Zustand or Vue with Pinia?
Raw framework rendering speed favors Vue in most 2026 benchmarks, with Vue 3.5’s Vapor Mode completing large DOM updates roughly 36% faster than React 19.2 in published tests. That said, state management library overhead is a small fraction of total render time in real apps; your component structure and re-render patterns usually matter more than which store library you pick.
Do I need TypeScript for this tutorial to work?
No, every pattern shown works in plain JavaScript too. TypeScript is recommended because Redux Toolkit, Zustand, and Pinia all ship first-class type inference, and skipping types means losing autocomplete on your state shape, which is one of the biggest quality-of-life wins these libraries offer over vanilla Context API or component state.
How long does it take to migrate a mid-size app from Redux to Zustand?
There’s no universal number since it depends on how many slices and middleware dependencies exist, but teams doing incremental, slice-by-slice migrations (as described in the Advanced Tips section above) typically spread the work across several sprints rather than attempting it in one release. Whatever timeline you land on, keep both libraries’ DevTools open during the transition so you can confirm the migrated slice produces identical state to the old reducer before deleting the old code path.
What’s the smallest state management option if I don’t need any of these libraries?
React’s built-in Context API combined with useReducer, or Vue’s built-in reactive() exported from a plain composable file, cover small apps without any extra dependency. Reach for Zustand, Redux Toolkit, or Pinia once you have state shared across more than a handful of unrelated components, or once you need DevTools-level debugging.
Is Pinia compatible with Nuxt?
Yes, Pinia is the state management library officially supported inside Nuxt 3 via the @pinia/nuxt module, and stores you build following this tutorial’s pattern work without modification inside a Nuxt project.


