Minimizing Shared State with React Hooks: The Wormhole State Management Pattern
This article explains how to keep state as close as possible to where it is used, using props for simple sharing, React Context as a "wormhole" for widely needed state, and custom hooks to simplify the pattern, illustrated with a step‑by‑step click‑counter example and performance considerations.
Wormhole State Management Pattern
State should be kept as close to its usage as possible. If a single component needs the state, pass it via props. If several components need it, still use props. When many components need the same data, place it in a React Context, which acts like a wormhole that bends the component tree so distant parts can communicate. Custom hooks simplify consuming the context.
Example: Click Counter
Step 1 – useState
const ClickCounter = () => {
const [count, setCount] = useState(0);
const onClick = () => setCount(c => c + 1);
return <button onClick={onClick}>{count} +1</button>;
};State count holds the click number; setCount updates it.
Step 2 – Reusable button
const ClickCounter = () => {
const [count, setCount] = useState(0);
const onClick = () => setCount(c => c + 1);
return (
<>
<p>You have clicked buttons {count} times</p>
<PrettyButton onClick={onClick}>+1</PrettyButton>
</>
);
};The state remains in ClickCounter while UI uses a shared PrettyButton component.
Step 3 – Grouped state
const ClickCounter = () => {
const [count, setCount] = useState({A: 0, B: 0});
const onClickA = () => setCount(c => ({...c, A: c.A + 1}));
const onClickB = () => setCount(c => ({...c, B: c.B + 1}));
return (
<>
<p>You have clicked A: {count.A}, B: {count.B} times</p>
<PrettyButton onClick={onClickA}>A +1</PrettyButton>
<PrettyButton onClick={onClickB}>B +1</PrettyButton>
</>
);
};State is now an object {A, B}, allowing a single state to hold multiple values. React re‑renders by shallowly comparing the whole state object; performance degrades around 10 000 elements.
For more complex updates, useReducer is recommended:
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'A': return {...state, A: state.A + 1};
case 'B': return {...state, B: state.B + 1};
default: return state;
}
}, {A: 0, B: 0});Step 4 – Prop‑drilling problem
const AlternativeClick = ({count, setCount}) => {
const onClick = () => setCount(c => ({...c, B: c.B + 1}));
return (
<div>
You can also update B here
<PrettyButton onClick={onClick}>B +1</PrettyButton>
<p>It's {count.B} btw</p>
</div>
);
};Passing count and setCount through many layers makes the component hard to move and understand.
Step 5 – Wormhole with custom hook
Define a context provider that stores a rich state object and a setter that updates a specific key:
export const SharedCountProvider = ({children}) => {
const [state, setState] = useState({A: 0, B: 0});
const [contextValue, setContextValue] = useState({
state,
setSharedCount: (key, val) => {
setState(s => ({...s, [key]: val}));
},
});
useEffect(() => {
setContextValue(cv => ({...cv, state}));
}, [state]);
return (
<SharedCountContext.Provider value={contextValue}>
{children}
</SharedCountContext.Provider>
);
};Consume the context via a custom hook:
export function useSharedCount() {
const {state, setSharedCount} = useContext(SharedCountContext);
const incA = () => setSharedCount('A', state.A + 1);
const incB = () => setSharedCount('B', state.B + 1);
return {count: state, incA, incB};
}Components can now access shared state without prop‑drilling:
const AlternativeClick = () => {
const {count, incB} = useSharedCount();
return (
<div>
You can also update B here
<PrettyButton onClick={incB}>B +1</PrettyButton>
<p>It's {count.B} btw</p>
</div>
);
};Performance considerations
Keep the shared state minimal and use separate context providers for different parts of the app. Avoid making the context global unless required; wrap only the smallest necessary subtree.
Complexity
Maintain low complexity; do not add unnecessary features.
Alternative implementations
The provider logic can be replaced with useReducer, XState, or Redux if those libraries better suit the project.
Real‑world usage (Sentry)
Sentry’s codebase includes organizationContext.tsx, which follows this pattern:
https://github.com/getsentry/sentry/blob/master/static/app/views/organizationContext.tsx
References
https://swizec.com/blog/wormhole-state-management/
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
