# Upgrading from v6 to v7
Easy Peasy v7 modernises the library around React 19, adopting the new
concurrent primitives (useTransition, useDeferredValue, useOptimistic,
use, and Suspense for asynchronous rehydration). The public store/model API
is unchanged — most consumers should be able to upgrade without touching their
store definitions.
This guide walks you through the breaking changes and the new APIs.
# Quick checklist
- Upgrade
reactandreact-domto^19. - Upgrade
easy-peasyto^7. - Wrap any subtree that uses
useStoreRehydratedin a<Suspense>(opens new window) boundary; the hook now suspends instead of returningfalsewhile rehydrating. - (Optional) Adopt the new concurrent hooks where they help —
useStoreTransition,useStoreDeferredState,useStoreOptimistic. - (Optional) If you build stores in React Server Components, edge runtimes,
or other React-free environments, switch those imports to
easy-peasy/server.
# Breaking changes
# React 19 is required
The peer dependency range is now react@^19 and react-dom@^19. v6 supported
React 18 and 19; v7 drops the React 18 support so the library can use
useOptimistic, use, and a simpler Suspense-based rehydration story
internally.
If you are not yet on React 19, stay on easy-peasy@^6.
# useStoreRehydrated now suspends
Prior to v7, useStoreRehydrated
returned a boolean — false while rehydration was in flight, true once
complete. Call sites typically gated rendering on the boolean:
// v6
function App() {
const isRehydrated = useStoreRehydrated();
return isRehydrated ? <Main /> : <div>Loading...</div>;
}
In v7 the hook suspends instead. Replace the conditional with a
<Suspense> (opens new window) boundary:
// v7
import { Suspense } from 'react';
function Main() {
useStoreRehydrated();
return <App />;
}
function Root() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Main />
</Suspense>
);
}
The hook still returns true once it resolves, so existing call sites that
read the boolean continue to compile — but the "loading" branch is now
unreachable and should be removed in favour of the Suspense fallback.
If you previously wrote a wrapper component:
- function WaitForStateRehydration({ children }) {
- const isRehydrated = useStoreRehydrated();
- return isRehydrated ? children : null;
- }
+ function WaitForStateRehydration({ children }) {
+ useStoreRehydrated();
+ return children;
+ }
createRoot(document.getElementById('app')).render(
<StoreProvider store={store}>
+ <Suspense fallback={<div>Loading...</div>}>
<WaitForStateRehydration>
<App />
</WaitForStateRehydration>
+ </Suspense>
</StoreProvider>,
);
Why this changed: in React 19 the idiomatic way to gate UI on an async resource is via
use()+<Suspense>. The previous boolean-flag pattern required a state subscription on every consumer; the Suspense pattern lets React deliver the rehydrated tree in a single render once the data is ready, with no flash of default state.
# easy-peasy/proxy-polyfill subpath removed
v6 shipped an easy-peasy/proxy-polyfill subpath which called immer's
enableES5() helper to support environments without native Proxy (notably
IE11). Easy Peasy v7 upgrades to immer v11, which dropped ES5 mode entirely —
Proxy is now the only supported backend.
If you imported the subpath:
- import 'easy-peasy/proxy-polyfill';
…remove the import. Every browser supported by React 19 has native Proxy, so
no replacement is needed.
The easy-peasy/map-set-support subpath (which calls enableMapSet()) is
unchanged — Map/Set draft support is still opt-in in immer v11.
# New APIs
# useStoreTransition
Wraps action dispatches in
startTransition (opens new window) and
exposes an isPending flag for non-urgent updates. Use it for actions that
trigger expensive re-renders (large list updates, sort/filter changes, thunks
with multiple incremental commits).
import { useStoreTransition } from 'easy-peasy';
function FetchButton() {
const [fetchItems, isPending] = useStoreTransition(
(actions) => actions.fetchItems,
);
return (
<button disabled={isPending} onClick={() => fetchItems()} type="button">
{isPending ? 'Loading...' : 'Fetch'}
</button>
);
}
The selector can return either a single function or an object of functions; function leaves are wrapped recursively. Wrapped thunks return their original promise. See the reference docs for more.
# useStoreDeferredState
Combines useStoreState with
useDeferredValue (opens new window) so
that React can keep returning the previous selected value while a fresh value
is being computed. Useful for expensive selectors where a stale-while-fresh
read is acceptable.
import { useStoreDeferredState } from 'easy-peasy';
function Results({ query }) {
const matches = useStoreDeferredState((state) =>
state.products.items.filter((p) => p.name.includes(query)),
);
return <ul>{matches.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}
See the reference docs for more.
# useStoreOptimistic
Combines useStoreState with
useOptimistic (opens new window). Components
can render an optimistic value while a pending action is in flight; once the
underlying state changes the optimistic value is discarded.
import { startTransition } from 'react';
import { useStoreActions, useStoreOptimistic } from 'easy-peasy';
function TodoList() {
const [items, addOptimistic] = useStoreOptimistic(
(state) => state.todos.items,
(current, pending) => [...current, pending],
);
const addItemAsync = useStoreActions((a) => a.todos.addItemAsync);
return (
<button
onClick={() => {
startTransition(async () => {
addOptimistic({ id: 'new', text: 'Hello' });
await addItemAsync({ text: 'Hello' });
});
}}
type="button"
>
Add
</button>
);
}
See the reference docs for more.
# easy-peasy/server subpath export
A React-free entry point that exposes the store, helpers, and types but none
of the React hooks or StoreProvider. Use it inside React Server Components,
edge runtimes, or any context where importing React would be incorrect.
import { createStore, action } from 'easy-peasy/server';
See the reference docs for more, including
an end-to-end RSC + client-hydration example. For App Router specifics —
where to put 'use client', the per-request store rule, and Suspense +
streaming behaviour — see the
Usage with RSC / Next App Router
recipe.
# Internal changes worth knowing
These don't change the public API but may affect the runtime feel:
- Rehydration
replaceStateis wrapped instartTransition. When persisted data lands, the resulting state replacement is marked as a transition, so the rehydration render is non-blocking and never preempts urgent input updates. No code changes are required to take advantage of this. useStoreStateuses React'suseSyncExternalStoredirectly (with a selector + memoisation layer on top). The behaviour is observably the same as in v6, but the implementation no longer ships the legacyuse-sync-external-storeshim.
If you hit anything that this guide doesn't cover, please open an issue (opens new window).