This guide deep-dives into React's core state management (useState) and advanced global state solutions like Redux, specifically addressing how data persists (or doesn't) across page refreshes. We'll uncover React's rendering mechanics and how they interact with external state and browser storage.
At its simplest, useState manages component-local state. It's perfect for UI-specific data that doesn't need to be shared widely or survive page refreshes.
- Pros: Simple, direct, no boilerplate.
- Cons: State is lost when the component unmounts or the page refreshes. Not suitable for global or persistent data without manual browser storage integration.
For application-wide or persistent state, more robust solutions are needed. This is where Redux often comes into play, bringing its own set of interactions with React's lifecycle and persistence strategies.
React processes state changes through two distinct phases:
| Phase | Nature | What happens? |
|---|---|---|
| 1. Render Phase | Asynchronous / Pure | React calls components to determine what should change (Virtual DOM diffing). Can pause/restart. |
| 2. Commit Phase | Synchronous | React applies actual changes to the real DOM (insert, update, delete). Never interrupted to ensure UI consistency. |
Even if Redux updates state synchronously, React might batch multiple updates, delaying the Render phase for performance. This explains why console.log(state) after dispatch sometimes shows the old value.
This diagram illustrates a Redux action's journey through React's Render/Commit phases and eventual disk persistence.
sequenceDiagram
participant Redux as Redux Store (Sync)
participant Render as React Render Phase (Async)
participant Commit as React Commit Phase (Sync)
participant Disk as Browser Storage (Async)
Note over Redux, Disk: 1. The Update Flow
Redux->>Redux: Reducer updates state immediately
Redux->>Render: Trigger Re-render
Note right of Render: React diffs Virtual DOM
Render->>Commit: Apply changes to Real DOM
Commit->>Disk: Persistence Middleware writes to storage
Note over Redux, Disk: 2. The Refresh/Rehydration Flow
Disk->>Redux: Read data & Dispatch REHYDRATE
Redux->>Redux: Store state merges with persisted data
Redux->>Render: New state triggers Render
Render->>Commit: UI updates to show restored data
- Redux is Synchronous: Dispatched actions immediately update the store's state.
- React is "Scheduled" (Async): React receives state change signals but may batch updates, delaying the Render phase for performance.
- Persistence is a Side-Effect: Writing to
localStoragehappens after Redux updates. Data might not hit disk instantly.
- User refreshes page.
- Browser clears all JS Heap Memory (React, Redux state are destroyed).
- HTML/JS loads.
- Redux Store initializes with initialState.
redux-persist(or custom code) triggers an Async Read fromlocalStorage.
- Persisted data is fetched from disk.
- A
REHYDRATEaction is dispatched to Redux. - React Render/Commit: React updates the UI to show the restored session.
React's Commit Phase completes before Rehydration finishes, showing an "empty" UI momentarily.
- Fix: Use
PersistGateto block React's Commit Phase until disk-read is complete.
Updating state during React's Commit Phase (e.g., useLayoutEffect) can cause double-writes to storage.
- Fix: Configure persistence middleware to "debounce" writes, preventing excessive I/O.
JSON.stringify (for persistence) deletes functions/symbols and converts complex objects (like Date()) to strings. Rehydration won't restore original object types.
- Example:
Date()saved as string, used asstate.date.toISOString()after rehydration, crashes. - Fix: Implement rehydration transforms to convert data back to original object types.
In SSR, the Render Phase happens on the Server, but Disk (localStorage) is client-only.
- Server Render: React renders with "Initial State" (e.g.,
loading: true). - Client Hydration: React takes over DOM.
- Rehydration:
redux-persistreads disk, updates state. - Second Render: React renders with "Actual State."
Warning: This can cause a "Hydration Error" if Server HTML differs from Client HTML.
- Solution: Use
useEffectto ensure persistence logic runs only after the first client-side commit, avoiding server/client mismatch.
React state (useState) lives in volatile memory, lost on refresh. For persistence, Redux (a synchronous state machine) can be used, but interacts with React's two-phase system: Render (async/diffing) and Commit (sync/DOM updates). Persistence mechanisms intercept Redux updates, writing state to permanent browser storage. On refresh, we block React's Commit Phase (via a gate like PersistGate) until data is rehydrated into Redux, ensuring a seamless, flicker-free persistent experience.