React State Management - Manage State Like a Pro
When your app grows, managing state with just useState
quickly becomes chaos. We're going to see how to structure and share state efficiently.
That's where stores (or global state management systems) come in.
The State Management Problem
What is State?
State is all the data that can change in your application:
- User data (profile, preferences)
- Interface state (open modals, loading)
- Business data (products, orders)
- API cache
The Problems That Arise
With useState
alone, you'll quickly encounter these problems:
// ❌ Problem: Prop Drilling
function App() {
const [user, setUser] = useState(null)
const [cart, setCart] = useState([])
const [theme, setTheme] = useState('light')
return (
<Header user={user} theme={theme} setTheme={setTheme} />
<ProductList cart={cart} setCart={setCart} user={user} />
<Footer theme={theme} />
)
}
function Header({ user, theme, setTheme }) {
return (
<header>
<UserProfile user={user} />
<ThemeToggle theme={theme} setTheme={setTheme} />
</header>
)
}
// You pass the props down 5 levels... 😱
The Solutions We'll See
- Context API → to share state without prop drilling
- Zustand → simple and modern state manager
- Redux Toolkit → for large, complex apps
- Jotai → atomic approach
- TanStack Query → for server data
Context API - The Native React Solution
Basic Concept
The Context API allows you to share data throughout the app without passing it as props.
// 1. Create a Context
const UserContext = createContext()
// 2. Provide the data (Provider)
function App() {
const [user, setUser] = useState(null)
return (
<UserContext.Provider value={{ user, setUser }}>
<Header />
<MainContent />
</UserContext.Provider>
)
}
// 3. Consume the data
function Header() {
const { user } = useContext(UserContext)
return <h1>Hello {user?.name}!</h1>
}
Complete Example: Theme Provider
import { createContext, useContext, useState } from 'react'
// 1. Create the Context
const ThemeContext = createContext()
// 2. Custom Hook to use the context
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// 3. Provider Component
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
const value = {
theme,
setTheme,
toggleTheme,
isDark: theme === 'dark'
}
return (
<ThemeContext.Provider value={value}>
<div className={`app theme-${theme}`}>
{children}
</div>
</ThemeContext.Provider>
)
}
// 4. Usage in components
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === 'light' ? '🌙' : '☀️'}
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
)
}
function Header() {
const { isDark } = useTheme()
return (
<header className={isDark ? 'header-dark' : 'header-light'}>
<h1>My App</h1>
<ThemeToggle />
</header>
)
}
// 5. Setup in App.jsx
function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
</ThemeProvider>
)
}
Multiple Contexts with Composition
// Auth Context
const AuthContext = createContext()
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (email, password) => {
setIsLoading(true)
try {
// API Call
const userData = await api.login(email, password)
setUser(userData)
} catch (error) {
throw error
} finally {
setIsLoading(false)
}
}
const logout = () => {
setUser(null)
localStorage.removeItem('token')
}
return (
<AuthContext.Provider value={{
user,
login,
logout,
isLoading,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
)
}
// Cart Context
const CartContext = createContext()
export function CartProvider({ children }) {
const [items, setItems] = useState([])
const addItem = (product) => {
setItems(prev => {
const existing = prev.find(item => item.id === product.id)
if (existing) {
return prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
return [...prev, { ...product, quantity: 1 }]
})
}
const removeItem = (productId) => {
setItems(prev => prev.filter(item => item.id !== productId))
}
const updateQuantity = (productId, quantity) => {
if (quantity <= 0) {
removeItem(productId)
return
}
setItems(prev =>
prev.map(item =>
item.id === productId
? { ...item, quantity }
: item
)
)
}
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
return (
<CartContext.Provider value={{
items,
addItem,
removeItem,
updateQuantity,
total,
itemCount: items.reduce((sum, item) => sum + item.quantity, 0)
}}>
{children}
</CartContext.Provider>
)
}
// Composition of providers
function App() {
return (
<AuthProvider>
<ThemeProvider>
<CartProvider>
<Router>
<Layout />
</Router>
</CartProvider>
</ThemeProvider>
</AuthProvider>
)
}
Custom Hooks for Contexts
// hooks/useAuth.js
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// hooks/useCart.js
export function useCart() {
const context = useContext(CartContext)
if (!context) {
throw new Error('useCart must be used within CartProvider')
}
return context
}
// Simplified usage
function ProductCard({ product }) {
const { addItem } = useCart()
const { isAuthenticated } = useAuth()
const handleAddToCart = () => {
if (!isAuthenticated) {
alert('Log in to add to cart')
return
}
addItem(product)
}
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={handleAddToCart}>
Add to cart
</button>
</div>
)
}
```## Zustand - Modern and Simple State Manager
### Why Zustand?
**Zustand** (= "state" in German) is an **ultra simple** and **performant** state manager:
- 📦 **Tiny**: 2.5kb gzipped
- 🚀 **Fast**: no providers needed
- 🔧 **Simple**: minimalist API
- 🎯 Native **TypeScript**
### Installation
```bash
npm install zustand
Basic Store
import { create } from 'zustand'
// Create a store
const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
// Use the store (anywhere in the app)
function Counter() {
const { count, increment, decrement, reset } = useCounterStore()
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
// In another component (no provider needed!)
function CounterDisplay() {
const count = useCounterStore((state) => state.count)
return <p>Current value: {count}</p>
}
Complex Store: E-commerce
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
// Interface for TypeScript (optional)
interface Product {
id: string
name: string
price: number
image: string
}
interface CartItem extends Product {
quantity: number
}
interface CartStore {
items: CartItem[]
isOpen: boolean
addItem: (product: Product) => void
removeItem: (productId: string) => void
updateQuantity: (productId: string, quantity: number) => void
clearCart: () => void
toggleCart: () => void
total: number
itemCount: number
}
// Store with middleware
const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
isOpen: false,
addItem: (product) => set((state) => {
const existingItem = state.items.find(item => item.id === product.id)
if (existingItem) {
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
} else {
return {
items: [...state.items, { ...product, quantity: 1 }]
}
}
}),
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
updateQuantity: (productId, quantity) => set((state) => {
if (quantity <= 0) {
return {
items: state.items.filter(item => item.id !== productId)
}
}
return {
items: state.items.map(item =>
item.id === productId
? { ...item, quantity }
: item
)
}
}),
clearCart: () => set({ items: [] }),
toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
// Computed values
get total() {
return get().items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
},
get itemCount() {
return get().items.reduce((sum, item) => sum + item.quantity, 0)
}
}),
{
name: 'cart-storage', // localStorage key
partialize: (state) => ({ items: state.items }) // Only persist items
}
),
{ name: 'cart-store' } // Name for Redux DevTools
)
)
// Usage in components
function ProductCard({ product }) {
const addItem = useCartStore((state) => state.addItem)
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => addItem(product)}>
Add to cart
</button>
</div>
)
}
function CartSummary() {
const { itemCount, total, toggleCart } = useCartStore()
return (
<button onClick={toggleCart} className="cart-button">
🛒 {itemCount} items - {total.toFixed(2)}€
</button>
)
}
function CartModal() {
const { items, isOpen, removeItem, updateQuantity, clearCart, toggleCart } = useCartStore()
if (!isOpen) return null
return (
<div className="cart-modal">
<div className="cart-content">
<div className="cart-header">
<h2>Your cart</h2>
<button onClick={toggleCart}>❌</button>
</div>
{items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<>
{items.map(item => (
<div key={item.id} className="cart-item">
<img src={item.image} alt={item.name} />
<div>
<h4>{item.name}</h4>
<p>{item.price}€</p>
</div>
<div className="quantity-controls">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
</div>
<button onClick={() => removeItem(item.id)}>
🗑️
</button>
</div>
))}
<div className="cart-actions">
<button onClick={clearCart} className="btn-secondary">
Clear cart
</button>
<button className="btn-primary">
Order ({total.toFixed(2)}€)
</button>
</div>
</>
)}
</div>
</div>
)
}
Store with async actions
const useProductStore = create((set, get) => ({
products: [],
isLoading: false,
error: null,
// Synchronous action
setProducts: (products) => set({ products }),
// Asynchronous action
fetchProducts: async () => {
set({ isLoading: true, error: null })
try {
const response = await fetch('/api/products')
if (!response.ok) throw new Error('Error loading')
const products = await response.json()
set({ products, isLoading: false })
} catch (error) {
set({ error: error.message, isLoading: false })
}
},
// Filter products
searchProducts: (query) => {
const { products } = get()
return products.filter(product =>
product.name.toLowerCase().includes(query.toLowerCase())
)
},
// Find a product
getProductById: (id) => {
const { products } = get()
return products.find(product => product.id === id)
}
}))
// Custom hook for filtered products
function useProductSearch(query = '') {
return useProductStore((state) =>
query ? state.searchProducts(query) : state.products
)
}
// Usage
function ProductList() {
const [searchQuery, setSearchQuery] = useState('')
const { isLoading, error, fetchProducts } = useProductStore()
const products = useProductSearch(searchQuery)
useEffect(() => {
fetchProducts()
}, [])
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
return (
<div>
<input
type="text"
placeholder="Search for a product..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
)
}
Slices (split the store)
// For large stores, we can split into slices
// authSlice.js
const createAuthSlice = (set, get) => ({
user: null,
token: null,
isAuthenticated: false,
login: async (email, password) => {
try {
const response = await api.login(email, password)
set({
user: response.user,
token: response.token,
isAuthenticated: true
})
} catch (error) {
throw error
}
},
logout: () => set({
user: null,
token: null,
isAuthenticated: false
})
})
// cartSlice.js
const createCartSlice = (set, get) => ({
items: [],
total: 0,
addToCart: (product) => {
// Logic...
}
})
// Main store
const useAppStore = create((set, get) => ({
...createAuthSlice(set, get),
...createCartSlice(set, get)
}))
Redux Toolkit - For large applications
When to use Redux?
Redux is useful for:
- Large teams (10+ developers)
- Very complex apps (100+ components)
- Complex business logic (state machines)
- Advanced debugging (time travel, replay)
Installation
npm install @reduxjs/toolkit react-redux
Basic Store with RTK
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
increment: (state) => {
state.value += 1 // Immer allows direct mutation
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
reset: (state) => {
state.value = 0
}
}
})
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions
export default counterSlice.reducer
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
// main.jsx
import { Provider } from 'react-redux'
import { store } from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
// Usage in a component
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, reset } from './store/counterSlice'
function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
)
}
RTK Query for API calls
// api/productsApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api/',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
}
}),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query({
query: () => 'products',
providesTags: ['Product']
}),
getProductById: builder.query({
query: (id) => `products/${id}`,
providesTags: (result, error, id) => [{ type: 'Product', id }]
}),
createProduct: builder.mutation({
query: (newProduct) => ({
url: 'products',
method: 'POST',
body: newProduct
}),
invalidatesTags: ['Product']
}),
updateProduct: builder.mutation({
query: ({ id, ...patch }) => ({
url: `products/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [{ type: 'Product', id }]
}),
deleteProduct: builder.mutation({
query: (id) => ({
url: `products/${id}`,
method: 'DELETE'
}),
invalidatesTags: ['Product']
})
})
})
export const {
useGetProductsQuery,
useGetProductByIdQuery,
useCreateProductMutation,
useUpdateProductMutation,
useDeleteProductMutation
} = productsApi
// store/index.js
import { configureStore } from '@reduxjs/toolkit'
import { productsApi } from '../api/productsApi'
export const store = configureStore({
reducer: {
[productsApi.reducerPath]: productsApi.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productsApi.middleware)
})
// Component
function ProductList() {
const { data: products, error, isLoading } = useGetProductsQuery()
const [deleteProduct] = useDeleteProductMutation()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div className="products-grid">
{products?.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.price}€</p>
<button onClick={() => deleteProduct(product.id)}>
Delete
</button>
</div>
))}
</div>
)
}
Jotai - Atomic Approach
Concept of Atoms
Jotai uses atoms: small, independent units of state.
npm install jotai
import { atom, useAtom } from 'jotai'
// Create atoms
const countAtom = atom(0)
const nameAtom = atom('Andy')
function Counter() {
const [count, setCount] = useAtom(countAtom)
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
function Greeting() {
const [name, setName] = useAtom(nameAtom)
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
<p>Hello {name}!</p>
</div>
)
}
Derived Atoms
import { atom, useAtom, useAtomValue } from 'jotai'
// Base atoms
const firstNameAtom = atom('Andy')
const lastNameAtom = atom('Cin')
// Derived (computed) atom
const fullNameAtom = atom((get) => {
const firstName = get(firstNameAtom)
const lastName = get(lastNameAtom)
return `${firstName} ${lastName}`
})
// Derived atom with setter
const upperCaseNameAtom = atom(
(get) => get(fullNameAtom).toUpperCase(),
(get, set, newValue) => {
const [first, last] = newValue.split(' ')
set(firstNameAtom, first)
set(lastNameAtom, last)
}
)
function NameForm() {
const [firstName, setFirstName] = useAtom(firstNameAtom)
const [lastName, setLastName] = useAtom(lastNameAtom)
const fullName = useAtomValue(fullNameAtom)
const [upperName, setUpperName] = useAtom(upperCaseNameAtom)
return (
<div>
<input
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First name"
/>
<input
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last name"
/>
<p>Full name: {fullName}</p>
<p>In uppercase: {upperName}</p>
<button onClick={() => setUpperName('JOHN DOE')}>
Change to John Doe
</button>
</div>
)
}
Atoms with storage
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// Persist in localStorage
const themeAtom = atomWithStorage('theme', 'light')
// Atom for user preferences
const userPrefsAtom = atomWithStorage('userPrefs', {
theme: 'light',
language: 'fr',
notifications: true
})
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom)
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Mode {theme === 'light' ? 'dark' : 'light'}
</button>
)
}
TanStack Query
Key Concept
TanStack Query (formerly React Query) is perfect for managing:
- Cache of API data
- Server/client synchronization
- Automatic Loading states
- Optimistic updates
npm install @tanstack/react-query
Basic Setup
// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router>
<Layout />
</Router>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Queries (reading data)
import { useQuery, useQueryClient } from '@tanstack/react-query'
// Custom hook for products
function useProducts() {
return useQuery({
queryKey: ['products'],
queryFn: async () => {
const response = await fetch('/api/products')
if (!response.ok) throw new Error('Error loading')
return response.json()
}
})
}
// Hook for a specific product
function useProduct(productId) {
return useQuery({
queryKey: ['products', productId],
queryFn: async () => {
const response = await fetch(`/api/products/${productId}`)
if (!response.ok) throw new Error('Product not found')
return response.json()
},
enabled: !!productId // Do not execute if no ID
})
}
// Usage in components
function ProductList() {
const { data: products, isLoading, error } = useProducts()
if (isLoading) return <div>Loading products...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
function ProductDetail({ productId }) {
const { data: product, isLoading, error } = useProduct(productId)
if (isLoading) return <div>Loading product...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>{product.price}€</p>
</div>
)
}
Mutations (modifying data)
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newProduct) => {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct)
})
if (!response.ok) throw new Error('Error creating')
return response.json()
},
onSuccess: () => {
// Invalidate the products cache
queryClient.invalidateQueries({ queryKey: ['products'] })
alert('Product created successfully!')
},
onError: (error) => {
alert(`Error: ${error.message}`)
}
})
}
function useUpdateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, ...updates }) => {
const response = await fetch(`/api/products/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})
if (!response.ok) throw new Error('Error updating')
return response.json()
},
onSuccess: (updatedProduct) => {
// Update the cache directly
queryClient.setQueryData(['products', updatedProduct.id], updatedProduct)
queryClient.invalidateQueries({ queryKey: ['products'] })
}
})
}
// Usage
function CreateProductForm() {
const createProduct = useCreateProduct()
const [formData, setFormData] = useState({ name: '', price: 0 })
const handleSubmit = (e) => {
e.preventDefault()
createProduct.mutate(formData)
}
return (
<form onSubmit={handleSubmit}>
<input
placeholder="Product name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<input
type="number"
placeholder="Price"
value={formData.price}
onChange={(e) => setFormData({ ...formData, price: Number(e.target.value) })}
/>
<button
type="submit"
disabled={createProduct.isPending}
>
{createProduct.isPending ? 'Creating...' : 'Create product'}
</button>
</form>
)
}
Optimistic Updates
function useToggleFavorite() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ productId, isFavorite }) => {
const response = await fetch(`/api/products/${productId}/favorite`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isFavorite })
})
if (!response.ok) throw new Error('Erreur')
return response.json()
},
// Optimistic update
onMutate: async ({ productId, isFavorite }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['products', productId] })
// Snapshot the previous value
const previousProduct = queryClient.getQueryData(['products', productId])
// Optimistically update to the new value
queryClient.setQueryData(['products', productId], old => ({
...old,
isFavorite
}))
// Return a context object with the snapshotted value
return { previousProduct, productId }
},
// If the mutation fails, use the context returned from onMutate to roll back
onError: (err, variables, context) => {
queryClient.setQueryData(
['products', context.productId],
context.previousProduct
)
},
// Always refetch after error or success:
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({ queryKey: ['products', variables.productId] })
}
})
}
function FavoriteButton({ product }) {
const toggleFavorite = useToggleFavorite()
const handleToggle = () => {
toggleFavorite.mutate({
productId: product.id,
isFavorite: !product.isFavorite
})
}
return (
<button onClick={handleToggle}>
{product.isFavorite ? '❤️' : '🤍'} Favorite
</button>
)
}
```## Resources for further learning
### Official Documentation
- 📚 [Zustand](https://docs.pmnd.rs/zustand/getting-started/introduction)
- 🚀 [TanStack Query](https://tanstack.com/query/latest)
- 🔧 [Redux Toolkit](https://redux-toolkit.js.org/)
- ⚛️ [Jotai](https://jotai.org/)
### Comparisons and Guides
- 📊 [State Management Comparison](https://react-state-management.com/)
- 🎯 [When to use what](https://kentcdodds.com/blog/application-state-management-with-react)
- 🔍 [Performance comparisons](https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode)