Persist React useReducer State in sessionStorage
Persist React useReducer state in sessionStorage across reloads, with serialization guards so the UI survives a refresh without leaking secrets to storage.
This article is published in English.
Sources: How to Persist React useReducer State with sessionStorage
The Solution
Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product. Separate retrieval policy from generation policy. Poisoned documents can steer answers without changing model weights.
import { useCallback, useReducer, type Dispatch } from 'react'
import { useSessionStorage } from 'react-use'
type ReducerFn<State, Action> = (state: State, action: Action) => State
export default function useSessionStorageReducer<State, Action>(
key: string,
reducer: ReducerFn<State, Action>,
initialState: State
): [State, Dispatch<Action>] {
const [sessionStorageValue, setSessionStorageValue] = useSessionStorage(
`reducer:${key.replaceAll(/\s+/g, '_')}`,
initialState
)
const reducerLocalStorage = useCallback(
(state: State, action: Action) => {
const newState = reducer(state, action)
setSessionStorageValue(newState)
return newState
},
[reducer, setSessionStorageValue]
)
return useReducer(reducerLocalStorage, sessionStorageValue)
}
How It Works
Separate retrieval policy from generation policy. Poisoned documents can steer answers without changing model weights.
Quick Example Usage
Separate retrieval policy from generation policy. Poisoned documents can steer answers without changing model weights.
type State = { count: number }
type Action = { type: 'INCREMENT' } | { type: 'DECREMENT' }
function counterReducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 }
case 'DECREMENT':
return { count: state.count - 1 }
default:
return state
}
}
export function Counter() {
const [state, dispatch] = useSessionStorageReducer('counter_key', counterReducer, { count: 0 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
)
}
Grab the Code
Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments. Separate retrieval policy from generation policy. Poisoned documents can steer answers without changing model weights.
Operational checklist
Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.
Validate and sanitize ingested content. Treat untrusted corpora as an attack surface.
Persistence turns demos into systems: threads, humans-in-the-loop, and time-travel debugging depend on it.
Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent.
Write a short runbook: rotate keys, drain queues, roll back the last change.
For hardening note 0, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state.
Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.
Separate retrieval policy from generation policy. Poisoned documents can steer answers without changing model weights.
For hardening note 1, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state.
Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product.
Validate and sanitize ingested content. Treat untrusted corpora as an attack surface.