@malahimdev/global-state
v1.3.2
Published
Tiny global state manager for React/Next.js
Maintainers
Readme
🌍 @malahimdev/global-state
⚡ Tiny, zero-boilerplate global state manager for React & Next.js — inspired by Zustand. No Context. No Providers. 100% TypeScript. React 18+ ready.
🔗 Live Demo
- 🚀 Live app: malahimdev-global-states.vercel.app/test
- 💻 Source code: github.com/MalahimHaseeb/malahimdev-global-states-demo
The demo is a real Next.js App Router app (Tailwind + shadcn/ui + next-themes) with a working, live example of every feature below:
| Page | What it proves |
|---|---|
| /test | Dashboard linking to every test page |
| /test/counter-a → /test/counter-b | The same store shared across two completely different pages, updated via client-side navigation, no reload — proves state is truly global, not page-scoped |
| /test/user | Selectors — visible render counters show a component only re-renders when its selected field changes |
| /test/todos | Custom equalityFn on array state — a "no-op update" button creates a new array reference with identical content, and the render counter proves it's ignored |
Clone the demo repo if you want to see the full source for stores, selectors, and page structure in context, rather than isolated snippets.
🚀 Features
- 🧠 Zustand-like API – create multiple global stores with ease
- ⚙️ No Providers needed – works directly with
useSyncExternalStore - 🎯 Selectors – subscribe to just the slice of state you need, skip unrelated re-renders
- 🧩 Tiny & Fast – less than 1KB gzipped
- 💙 TypeScript first – full type inference
- 🌈 Works in Next.js App Router – perfect for modern projects
📦 Installation
npm install @malahimdev/global-state
# or
yarn add @malahimdev/global-state🧠 Basic Usage
1️⃣ Create a Store
Create your store inside /store/counter.ts:
"use client";
import { createGlobalStore } from "@malahimdev/global-state";
interface CounterState {
count: number;
}
export const counterStore = createGlobalStore<CounterState>({
count: 0,
});2️⃣ Use It Anywhere
You can access or update the state from any component or page:
"use client";
import { counterStore } from "@/store/counter";
export default function CounterPage() {
const [counter, setCounter] = counterStore.useStore();
return (
<div>
<h1>Count: {counter.count}</h1>
<button onClick={() => setCounter((prev) => ({ count: prev.count + 1 }))}>
Increment
</button>
</div>
);
}✅ No need to wrap your app in any provider — it just works.
🧩 Multiple Stores Example
Create multiple independent global stores easily.
// /store/user.ts
"use client";
import { createGlobalStore } from "@malahimdev/global-state";
interface UserState {
name: string;
loggedIn: boolean;
}
export const userStore = createGlobalStore<UserState>({
name: "Guest",
loggedIn: false,
});Use it side-by-side:
"use client";
import { counterStore } from "@/store/counter";
import { userStore } from "@/store/user";
export default function HomePage() {
const [counter, setCounter] = counterStore.useStore();
const [user, setUser] = userStore.useStore();
return (
<div>
<p>User: {user.name}</p>
<p>Count: {counter.count}</p>
<button onClick={() => setCounter((p) => ({ count: p.count + 1 }))}>
Increment
</button>
<button
onClick={() =>
setUser(user.loggedIn ? { loggedIn: false, name: "Guest" } : { loggedIn: true, name: "Malahim" })
}
>
{user.loggedIn ? "Logout" : "Login"}
</button>
</div>
);
}Access it in any component:
"use client";
import Link from "next/link";
import { counterStore } from "@/store/counter";
import { userStore } from "@/store/user";
export default function OtherPage() {
const [counter] = counterStore.useStore();
const [user] = userStore.useStore();
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-pink-600 via-purple-600 to-indigo-600 text-white px-6">
<div className="bg-white/10 p-6 sm:p-10 rounded-2xl text-center w-full max-w-md shadow-2xl">
<h1 className="text-3xl sm:text-5xl font-extrabold mb-6 text-yellow-300">
⚡ Live Global State
</h1>
<p className="text-lg mb-4">👤 User: {user.name}</p>
<p className="text-lg mb-6">🔥 Count: {counter.count}</p>
<Link href="/" className="text-indigo-200 underline">
← Back to Home
</Link>
</div>
</div>
);
}✅ Both pages stay perfectly synced — count and user changes reflect instantly!
🎯 Selectors (v1.3.0+)
By default, useStore() returns the whole state object, so a component re-renders on any change to that store — even fields it doesn't use. If your store holds more than one or two fields, pass a selector to subscribe to just the piece you care about.
// /store/user.ts
"use client";
import { createGlobalStore } from "@malahimdev/global-state";
interface UserState {
name: string;
loggedIn: boolean;
theme: "light" | "dark";
}
export const userStore = createGlobalStore<UserState>({
name: "Guest",
loggedIn: false,
theme: "light",
});"use client";
import { userStore } from "@/store/user";
// This component only cares about the name.
// It will NOT re-render when `loggedIn` or `theme` changes.
export default function Greeting() {
const [name] = userStore.useStore((state) => state.name);
return <p>Hello, {name} 👋</p>;
}"use client";
import { userStore } from "@/store/user";
// This component only cares about the theme.
export default function ThemeToggle() {
const [theme, setUser] = userStore.useStore((state) => state.theme);
return (
<button
onClick={() =>
setUser((prev) => ({
...prev,
theme: prev.theme === "light" ? "dark" : "light",
}))
}
>
Current theme: {theme}
</button>
);
}Now Greeting and ThemeToggle each re-render only when their slice changes — updating loggedIn elsewhere in the app won't touch either of them.
⚖️ Custom Equality (optional)
Sometimes Object.is isn't the right comparison — for example, if you're selecting an array or object and want to compare by content instead of reference. Pass a custom equality function as the second argument to useStore:
const [tags] = tagsStore.useStore(
(state) => state.tags,
(a, b) => a.length === b.length && a.every((t, i) => t === b[i])
);You can also set a store-wide default equality function when creating the store:
export const listStore = createGlobalStore<{ items: string[] }>(
{ items: [] },
{
equalityFn: (a, b) => JSON.stringify(a) === JSON.stringify(b),
}
);♻️ Resetting State
Every store comes with a reset() method that restores it to its original initialState — handy for logout flows, clearing filters, or resetting forms.
"use client";
import { userStore } from "@/store/user";
export default function LogoutButton() {
return <button onClick={() => userStore.reset()}>Log out</button>;
}🧵 Reading State Outside React
getState, setState, and subscribe work outside of components too — useful in event handlers, utility functions, or non-React code:
import { userStore } from "@/store/user";
// Read the current value directly
console.log(userStore.getState().name);
// Update it directly
userStore.setState((prev) => ({ ...prev, loggedIn: true }));
// Subscribe manually (e.g. for logging, analytics, or syncing to localStorage)
const unsubscribe = userStore.subscribe((state) => {
console.log("User store changed:", state);
});
// Call this when you're done listening
unsubscribe();⚠️ Good to Know
- All stores are client-side only — always mark files that create or use a store with
"use client"in Next.js App Router. - Keep
initialStatestatic and identical between server and client renders to avoid hydration mismatches. - For large or deeply nested state, prefer selectors over destructuring the whole state object, to avoid unnecessary re-renders.
📜 License
MIT © Malahim Haseeb (https://www.malahim.dev)
