This content originally appeared on DEV Community and was authored by HichemTech
Imagine This Situation…
You’re building a React app. You’ve got a counter, a user object, maybe a Firebase subscription. You think, hey I’ll just throw useState
in there and I’m good. And then it hits you: what if I need this same state in another component?
Options today?
- Context → wrap your whole
App.tsx
just to share one boolean. Boring. Annoying. Overkill. - Redux → congratulations, you just added 100 lines of boilerplate for 1 variable.
- Zustand, Jotai, etc. → cool, but still feels like setting up a store for something that could be a line of code.
So what if I told you that you can have a shared state in React with almost the same simplicity as useState
? No providers if you don’t want them, no reducers, no context wrappers, just a tiny hook that feels natural.
That’s what I built: react-shared-states
.
Check it here: react-shared-states
Example: Two Components, One Counter
import { useSharedState } from "react-shared-states";
function A() {
const [count, setCount] = useSharedState("counter", 0);
return <button onClick={() => setCount(count + 1)}>A: {count}</button>;
}
function B() {
const [count, setCount] = useSharedState("counter", 0);
return <button onClick={() => setCount(count + 1)}>B: {count}</button>;
}
function App() {
return (
<>
<A />
<B />
</>
);
}
Click on A’s button → B updates instantly. They’re synced because they share the same key ("counter"
). That’s it. No Provider, no boilerplate, no context-hating rants.
This is the heart of the package: shared states that feel like normal states.
How Did This Madness Start?
Story time. A friend of mine was using Firebase and came across a package called useBetween
. He loved it: it allowed him to call a hook once, and then reuse the data everywhere. For example:
const users = useBetween(useUsersData);
Perfect for his Firebase use case—load users once, and then just consume them anywhere. He was happy. Until he wasn’t.
He merged his branch and suddenly pnpm peer dependency errors everywhere. His branch was on React 18, but the main repo was updated to React 19. He tried updating, but nothing worked. Why? Because useBetween
was abandoned. Even worse, it was using React internals that React 19 removed.
One infamous line from the source code:
const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
Yes, you read that right. The author literally accessed React’s private internals and based the whole package on it.
My friend had no choice but to throw it away and refactor everything back to… Redux. LMAO. XDD. Can you imagine being forced back into reducers and actions after tasting freedom?
That’s when I thought: Why can’t we have something like useBetween
, but done properly?
The Secret Weapon: useSyncExternalStore
React actually gives us the right tool for this: useSyncExternalStore
. Unlike useBetween
, which broke React’s rules, this hook is the official API to subscribe to external stores.
Let me show you a tiny demo:
let counter = 0;
let listeners: (() => void)[] = [];
const subscribe = (listener: () => void) => {
listeners.push(listener);
return () => listeners = listeners.filter(l => l !== listener);
};
const getSnapshot = () => counter;
const setCounter = (val: number) => {
counter = val;
listeners.forEach(l => l());
};
function Counter() {
const value = useSyncExternalStore(subscribe, getSnapshot);
return <button onClick={() => setCounter(value + 1)}>{value}</button>;
}
Clicking the button updates the external counter, notifies all listeners, and React re-renders. It’s just like useState
, but the state lives outside React. That’s the magic: shared state, officially supported.
Scopes: Not Everything Should Be Global
Sometimes, you don’t want everything to be shared globally. That’s where scopes come in.
By default, useSharedState
uses the global scope. But you can isolate parts of your app using SharedStatesProvider
:
<SharedStatesProvider>
<Component />
</SharedStatesProvider>
Inside this provider, all shared states are local to that scope. Want to get fancy? You can even give a provider a name so multiple trees (like a modal in a portal) can share the same scope:
<SharedStatesProvider scopeName="modal">
<ModalContent />
</SharedStatesProvider>
<Portal>
<SharedStatesProvider scopeName="modal">
<FloatingToolbar />
</SharedStatesProvider>
</Portal>
Now your modal and toolbar are in sync, even though they’re in different React trees without having one big context provider. Clean, simple, and powerful.
Async Goodness: Shared Functions
What if your shared state comes from a fetch? Easy.
const fetchUser = () => fetch("/api/me").then((r) => r.json());
function Profile() {
const { state, trigger } = useSharedFunction("current-user", fetchUser);
useEffect(() => { trigger(); }, []);
if (state.isLoading && !state.results) return <p>Loading...</p>;
if (state.error) return <p>Error!</p>;
return <h1>{state.results.name}</h1>;
}
Call trigger()
once, and the result is cached and shared everywhere. Another component using the same key ("current-user"
) gets the result instantly, without refetching. That’s automatic caching and deduplication out of the box.
Subscriptions: Firestore, Sockets, You Name It
Realtime data? useSharedSubscription
is here:
const { state, trigger } = useSharedSubscription(
"user-123",
(set, onError, onComplete) => {
const unsub = firebase.onSnapshot(doc(db, "users", "123"), (snap) => {
set(snap.data());
}, onError, onComplete);
return unsub;
}
);
useEffect(() => trigger(), []);
if (state.isLoading) return <p>Connecting...</p>;
if (state.error) return <p>Error!</p>;
return <div>{state.data.name}</div>;
The first component sets up the subscription, others just “tap in” and get updates instantly. When no component uses it anymore, it auto-unsubscribes. No duplicate sockets, no memory leaks.
Static API: Control from Outside React
Sometimes you want to set a value or trigger a fetch without being inside a component. That’s why the library exposes a static API:
import { sharedStatesApi } from "react-shared-states";
sharedStatesApi.set("theme", "dark");
console.log(sharedStatesApi.get("theme"));
Great for SSR, debugging, or triggering updates from non-React code.
Wrapping Up
So that’s the story:
- My friend tried
useBetween
, it broke on React 19 because it hacked private internals. - I thought: why not do the same idea, but properly, using
useSyncExternalStore
? - After experimenting, I built
react-shared-states
→ a library that gives you shared states, scopes, async functions, subscriptions, and even static APIs, all with React’s blessing.
If you hate boilerplate, if you hate wrapping your app with context providers for tiny values, or if you just want something that works—you’ll love this package.
Because honestly… shared states in React have never been this easy, or this fun.
Check it out: github.com/HichemTab-tech/react-shared-states
This content originally appeared on DEV Community and was authored by HichemTech