Most React state libraries try to do too much. Redux ships an entire opinion about side effects, middleware, and devtools. Zustand is great but introduces a new mental model. MobX is reactive in ways that surprise you six months later. Sometimes you just want one place to share data between components without prop-drilling and without an architecture lecture.
That is what rebaz-dev-global-state is. It is a tiny React package that wraps Context + useReducer into a clean global-store API with two exports and no configuration. This guide walks through what it does, when to reach for it, and the tradeoffs to know about.
Install
npm install rebaz-dev-global-state --save
# or
yarn add rebaz-dev-global-state --save
The package depends on React 18.2.0. If your app is on a different React version, follow the version-replace steps in the package README — but the recommended path is to keep React on 18.2.0 across your project.
The package is on npm at the latest version 1.1.3 (MIT licensed, source at github.com/rebazomar121/rebaz-dev-global-state).
The 10-second mental model
There are two exports:
GlobalStateProvider— wrap your app once. Declare the state slots you’ll use up front via thestateNamesprop.GlobalStateContext— the React context.useContext(GlobalStateContext)gives you{ state, setData }.
That’s it. Everything else is just patterns on top of those two.
Step 1 — Wrap the app
In a Next.js _app.tsx:
import { GlobalStateProvider } from "rebaz-dev-global-state";
export default function MyApp({ Component, pageProps }) {
return (
<GlobalStateProvider stateNames={["user", "theme", "cart"]}>
<Component {...pageProps} />
</GlobalStateProvider>
);
}
Or in a Vite/CRA main.tsx:
import { GlobalStateProvider } from "rebaz-dev-global-state";
ReactDOM.createRoot(document.getElementById("root")!).render(
<GlobalStateProvider stateNames={["user", "theme", "cart"]}>
<App />
</GlobalStateProvider>
);
You list the state names you’ll use — user, theme, cart, etc. The provider creates a slot for each, initialised to null. You can also pass initialState if you want non-null defaults:
<GlobalStateProvider
stateNames={["user", "theme", "cart"]}
initialState={{ theme: "dark", cart: [] }}
>
Step 2 — Read and write from any component
import { useContext } from "react";
import { GlobalStateContext } from "rebaz-dev-global-state";
export default function Header() {
const { state, setData } = useContext(GlobalStateContext);
const user = state?.user;
const theme = state?.theme ?? "light";
return (
<header>
<span>{user?.name ?? "Guest"}</span>
<button onClick={() => setData("theme", theme === "dark" ? "light" : "dark")}>
Toggle theme
</button>
</header>
);
}
The API is two functions:
state[stateName]— read the slot. May benulluntil you set it.setData(stateName, value)— write the slot. Triggers a re-render in any component that consumes the same context.
Note the optional-chaining (state?.user). The context value is initialised by the provider, so state is always defined inside the tree — but TypeScript users will want a small wrapper hook for type safety (next section).
Step 3 — A typed wrapper hook (recommended)
The raw useContext API is JavaScript-shaped. For TypeScript, wrap it once:
// hooks/useGlobalState.ts
import { useContext } from "react";
import { GlobalStateContext } from "rebaz-dev-global-state";
type AppState = {
user: { id: string; name: string } | null;
theme: "dark" | "light";
cart: { id: string; qty: number }[];
};
export function useGlobalState() {
const ctx = useContext(GlobalStateContext) as {
state: AppState;
setData: <K extends keyof AppState>(name: K, value: AppState[K]) => void;
};
if (!ctx) throw new Error("useGlobalState must be used inside <GlobalStateProvider>");
return ctx;
}
Now consumers get full type-safety and autocomplete:
const { state, setData } = useGlobalState();
setData("theme", "dark"); // ✅ ok
setData("theme", "neon"); // ❌ type error
setData("user", { id: "1", name: "Ada" }); // ✅
This is the pattern I recommend for any non-trivial app — keep the raw context untouched, derive a typed hook per project.
How it actually works
The internals are intentionally tiny:
useReducerwith a single action type"SET_DATA".{ stateName, payload }action shape.- Reducer does
{ ...state, [stateName]: payload }.
That’s the whole engine. About 30 lines of code, zero dependencies beyond React. You can read the source in five minutes — and that is the point.
When to reach for it
It is the right tool when:
- You want shared state across the tree without prop-drilling.
- The state is a small, fixed set of slots (user, theme, current page, cart).
- You do not need middleware, time-travel debugging, or async-action handling.
- You want fewer lines than Redux and fewer concepts than Zustand.
Examples that fit perfectly:
- A theme switcher.
- Logged-in user info.
- A small global notification queue.
- Form-step coordination across a wizard.
When it’s the wrong fit
It is not the right tool when:
- You have dozens of slots that change at high frequency. Every
setDatare-renders every consumer of the context. With many slots and many subscribers, you’ll see unnecessary renders. Reach for Zustand or Jotai, which slice subscriptions per-key. - You need async middleware, optimistic updates, or undo/redo. Redux Toolkit is built for that.
- You need server state (caching, refetching, dedupe). React Query / SWR is the right shape.
- You need persistence to localStorage out of the box. Easy to add as a one-line
useEffect, but not built in.
The honest line: this package replaces ~80% of what people use Redux for in small-to-medium apps. The other ~20% has better tools.
Performance notes
- One Context, one consumer call. Every
setDatacauses every component readingGlobalStateContextto re-render. - For small apps, this doesn’t matter. For apps with hundreds of consumers, slice your context: split state into multiple providers (e.g.
UserProvider,UIProvider,CartProvider) so a change tocartdoesn’t re-render the user header. React.memoon consumer components helps when the props they actually read haven’t changed.
A complete worked example
// app/layout.tsx (or _app.tsx)
"use client";
import { GlobalStateProvider } from "rebaz-dev-global-state";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GlobalStateProvider
stateNames={["user", "theme"]}
initialState={{ theme: "dark" }}
>
{children}
</GlobalStateProvider>
</body>
</html>
);
}
// hooks/useGlobalState.ts — typed wrapper as above
// components/LoginForm.tsx
"use client";
import { useGlobalState } from "@/hooks/useGlobalState";
export function LoginForm() {
const { setData } = useGlobalState();
return (
<form
onSubmit={async (e) => {
e.preventDefault();
const res = await fetch("/api/login", { method: "POST" });
const user = await res.json();
setData("user", user);
}}
>
<button type="submit">Sign in</button>
</form>
);
}
// components/Header.tsx
"use client";
import { useGlobalState } from "@/hooks/useGlobalState";
export function Header() {
const { state, setData } = useGlobalState();
return (
<header>
<span>Hi, {state.user?.name ?? "Guest"}</span>
<button onClick={() => setData("theme", state.theme === "dark" ? "light" : "dark")}>
{state.theme === "dark" ? "☀️" : "🌙"}
</button>
</header>
);
}
That is a fully working theme + user system in roughly 40 lines, no Redux, no boilerplate.
Wrap up
Most state-management discussions are about whether you need more than what Context gives you. rebaz-dev-global-state is the answer to the opposite question: what if you need exactly Context + a single setter, with no boilerplate, on every project? That is a small, useful slot that the bigger libraries don’t fill cleanly.
Install it when the API surface looks right for the job. Outgrow it when you genuinely need slicing, middleware, or server state. Both are fine outcomes.