// 1. Dispatch Action
dispatch({ type: 'counter/increment' });
// 2. Reducer processes it
function reducer(state, action) {
if (action.type === 'counter/increment') {
return { value: state.value + 1 };
}
return state;
}
// 3. UI Updates via Selector
const count = useSelector(state => state.value);
Real World Example
// In a Component
<button onClick={() => dispatch(increment())}>
Count is {count}
</button>
dispatch(addItem(item)).RTK Improvement: Uses "Slices" to bundle actions and reducers. Uses "Immer" to allow writing "mutating" logic that is actually immutable.
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; }
}
});
Real World Example
// Async Thunk in Slice
export const fetchUser = createAsyncThunk('users/fetch', async (id) => {
const response = await api.getUser(id);
return response.data;
});
const userSlice = createSlice({
extraReducers: (builder) => {
builder.addCase(fetchUser.fulfilled, (state, action) => {
state.data = action.payload;
});
}
});
Zustand is a "bearbones" state management library.
import { create } from 'zustand'
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
// Usage
function BearCounter() {
const bears = useStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
Context API is best for:
Avoid Context for: High-frequency updates (like a text input or mouse position) because it triggers a re-render of all consumers every time the value changes, leading to performance issues.
React Query handles:
Doing this manually with useEffect is error-prone and verbose.
A strategy where the browser serves stale data from the cache instantly (for speed), while simultaneously fetching fresh data in the background to update the cache and UI.
const queryClient = useQueryClient();
// After a successful mutation (POST/PUT)
mutation.mutate(data, {
onSuccess: () => {
// Mark 'todos' as stale so they refetch immediately
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
Code that sits between dispatching an action and the moment it reaches the reducer. Used for logging, crash reporting, and async tasks (Thunks/Sagas).
A function that extracts specific pieces of data from the Redux store state. const selectCount = state => state.counter.value. Memoized selectors (Reselect) prevent re-computation if inputs haven't changed.
Structuring state like a database (ID-based lookup) rather than arrays.
Array: [{ id: 1, name: 'A' }] (Find is O(n))
Normalized: { ids: [1], entities: { 1: { name: 'A' } } } (Find is O(1))
Helps avoid duplication and makes updates easier.
They use an Atomic model. State is split into small "atoms". Components subscribe to specific atoms. This avoids the "top-down" provider issues of Context and the boilerplate of Redux. Great for apps with many interdependent derived states (like a spreadsheet).
Uses Observables and mutable data structures. It automatically tracks dependencies and updates components when data changes. "Anything that can be derived from the application state, should be."
A library for creating Finite State Machines (FSM). It makes state transitions explicit and robust. You define: Idle -> Loading -> Success/Error. It prevents impossible states (e.g., being "Loading" and "Success" at the same time).
Moving state from a child component to its parent so that the parent can pass it down to other children (siblings) that need it.
State that can be calculated from existing state or props. Do not store derived state in useState. Calculate it during render.
Bad: const [fullName, setFullName] = useState(first + last)
Good: const fullName = firstName + lastName
Sync your state store with localStorage or sessionStorage.
Redux: redux-persist.
Zustand: persist middleware.
React Query: persistQueryClient.
Updating the UI immediately as if the server request succeeded, before getting the actual response. If the request fails, you roll back the change. Makes the app feel instant.
Instead of passing user down 5 levels, pass the component that needs user as a child prop.
// Before
<Layout user={user} /> // Layout passes it to Header -> UserMenu
// After
<Layout header={<Header user={user} />} />
1. When the next state depends on the previous state in complex ways.
2. When you have multiple sub-values (objects) that change together.
3. When you want to separate state logic from the component (easier testing).
Data should exist in only one place. If multiple components need it, lift it up or put it in a store. Duplicating data leads to synchronization bugs.
For simple forms, useState is fine. For complex forms (validation, dirty fields, touched fields), use a library like React Hook Form or Formik. They handle the state management and performance optimization (uncontrolled inputs) for you.
Passing a callback function from a parent to a child. The child calls the function to update the parent's state. This is the standard way to communicate "up" the tree.
React relies on reference checks (oldState === newState) to decide if it should re-render. If you mutate an object directly, the reference stays the same, and React won't update. Always create a new object/array copy.
set merges state. get allows you to read the current state inside an action (e.g., to increment a value based on current value).
A browser extension that lets you inspect every action dispatched, view the state diff, and "time travel" (jump back to previous states) to debug logic errors.
Combining useReducer (for logic) and Context (for passing dispatch down) creates a lightweight global state management system without external libraries. Good for small apps.
The practice of keeping state as close to where it is used as possible. Don't make everything global. If only one component needs it, keep it local. If a subtree needs it, lift it to the common ancestor.
A hook introduced in React 18 for subscribing to external data sources (like Redux, Zustand, or browser APIs). It ensures consistent updates during concurrent rendering features (like Transitions) by preventing "tearing" (visual inconsistencies).
When different parts of the UI show different values for the same state at the same time. This can happen in concurrent rendering if an external store updates while React is in the middle of rendering a tree.
State that changes very rapidly but doesn't need to trigger a re-render for every change (e.g., tracking mouse position for a tooltip, or scroll position). Use useRef or direct DOM manipulation for performance, then sync to React state only when necessary.
Libraries wrap the state object in a JS Proxy. When you access a property (get), the library records that your component depends on it. When you change it (set), the library knows exactly which components to re-render. No manual selectors needed.
A race condition where a child component tries to read data from the store that has already been deleted by a parent's action, causing a crash. Modern react-redux hooks (useSelector) handle this automatically, but it was a common issue with connect.
When updating an immutable data structure, you copy the parts that changed, but keep references to the parts that didn't. This saves memory and makes equality checks fast. Libraries like Immer and Immutable.js use this.
Generally, keep state isolated within each micro-frontend. For shared state (Auth, Theme), use a custom event bus, window object, or a shared library instance (though this couples them). URL state is often the best way to coordinate.
Storing state in the URL (query params, route params) makes it shareable, bookmarkable, and handles browser history (back button) automatically. Great for search filters, pagination, and active tabs.
Passing data through many layers of components that don't use it.
Fixes:
1. Composition: Pass components as children.
2. Context API: Provide data at the top.
3. Global Store: Redux/Zustand.
Wrap the component in a test-specific Provider with a mocked store/initial state.
render(<Provider store={mockStore}><MyComponent /></Provider>).
Avoid testing implementation details of the store; test the UI outcome.
React groups multiple state updates into a single re-render for better performance. React 18 introduced "Automatic Batching" which batches updates even inside promises, timeouts, and native event handlers.
When a user triggers multiple requests quickly (e.g., typing), responses might arrive out of order.
Fix: Use a library like React Query (handles it automatically) or use a cleanup function in useEffect with a boolean flag to ignore stale responses.
The architecture Redux is based on. Action -> Dispatcher -> Store -> View. Data flows in one direction only, making it predictable and easier to debug than two-way binding (MVC).
Use it to cache the result of an expensive calculation derived from state/props. It runs during render. Do not use it for side effects. It helps prevent performance bottlenecks when the component re-renders.
1. Key Prop: Changing the key of a component forces React to destroy and recreate it, resetting all internal state.
2. Store Action: Dispatch a RESET action in Redux/Zustand that sets the state back to initialState.
State Management (Redux) is about storing data.
State Machine (XState) is about modeling the logic and valid transitions between states. You can use them together.
A method to prevent overwriting data. When updating a record, you send the version number you currently have. The server checks if that version matches the database. If not (someone else updated it), the request fails, and you must re-fetch and merge.
Deeply nested objects are hard to update immutably (spread hell).
Fixes:
1. Normalize: Flatten the structure.
2. Immer: Use Immer to write mutable-style logic.
3. Lenses/Optics: Functional programming concept (advanced).
useState: Updating the state triggers a re-render of the component.
useRef: Updating the .current property does not trigger a re-render. Useful for mutable values that don't affect the visual output (timers, DOM elements).