Overwatch now supports Server-Side Rendering SSR (v 1.0.7)
A minimal API with developer flexibility, TypeScript-first state-management solution, inspired by the simplicity of Zustand, It's a super lightweight yet expressive state management library for React Js & Next Js, built on the singleton design pattern. It offers support for global and instance-specific middlewares, immutability, batched updates, and custom event communication β all designed to be used without extensive boilerplate.
Goal with OverWatch was to prioritize reusability, a positive developer experience, and clear component-level state and event tracking.
Read the full documentation at overwatchts.in/docs.
OverWatch evolved from an internal utility that leveraged the singleton and pub-sub patterns. We've now refined it for React, incorporating hooks, strong typing, and automatic cleanup to make it accessible to everyone.
OverWatch is:
- Built with React hooks and TypeScript (offering 100% type safety for predictable development).
- Lightweight β you won't find context providers, reducers, or dispatches here.
- Ideal for managing both component-level and application-level shared state, providing flexibility for various needs.
- Inspired by Zustand: builds upon the simplicity and performance-first design of Zustand but reimagines it with a focus on advanced middleware, event-driven architecture, immutable state enforcement, and without the need for central store boilerplate.
If you've ever felt that Redux was too comprehensive for your needs, or that libraries like Zustand didn't offer enough specific control, OverWatch aims to provide a balanced approach. It combines a minimal API with flexibility to enhance your developer experience.
At its core, OverWatch uses a pub-sub pattern to manage state effectively:
- Every state key functions as a communication channel.
- Components can subscribe to updates on these channels, acting as listeners.
- You can publish changes to state, either globally or specifically.
- Middleware pipelines run before changes are published, allowing for custom logic.
- Batching ensures that multiple updates are grouped efficiently, optimizing performance.
- Immutability is maintained, helping your state remain consistent and easy to reason about.
To get started with OverWatch-TS, simply install it using npm or yarn:
# Using npm
npm install overwatch-ts
# Using yarn
yarn add overwatch-ts
This function initializes a shared state value globally. While optional, it's helpful for establishing a default state before any component mounts.
If
createSharedState
isn't used,useSharedState
will automatically create the key the first time it's accessed, providing a flexible starting point.
// Setting an initial theme
createSharedState('theme', { mode: 'dark' });
Use this hook in any component to read and update your shared state. It's your primary interface for interacting with OverWatch.
import { useSharedState } from 'overwatch-ts';
const ThemeSwitcher = () => {
const [theme, setTheme] = useSharedState<{ mode: string }>('theme');
const toggleTheme = () => {
setTheme({ mode: theme.mode === 'dark' ? 'light' : 'dark' });
};
return (
<button onClick={toggleTheme}>
Switch to {theme.mode === 'dark' ? 'Light' : 'Dark'} Mode
</button>
);
};
This hook allows you to extract only a specific part of a shared state object, which can help optimize component rendering by preventing unnecessary re-renders.
import { usePicker } from 'overwatch-ts';
const ThemeIndicator = () => {
const mode = usePicker('theme', t => t.mode);
return <div>{mode === 'dark' ? 'π' : 'βοΈ'}</div>;
};
Why and when to use?
- It helps prevent unnecessary re-renders, improving application performance.
- It's particularly useful for larger state objects where you only need to react to changes in one specific field.
Use this to attach one or more global middlewares to a shared state key. These middlewares will apply to all updates for that specific state key.
import { applyMiddleware, createSharedState } from 'overwatch-ts';
// A simple logger for all 'theme' state changes
const globalThemeLogger = (newValue, next) => {
console.log('Global Theme Change:', newValue);
next(newValue); // Remember to call 'next' to continue the update
};
createSharedState('theme', { mode: 'dark' });
applyMiddleware('theme', globalThemeLogger);
(NOTE: If both instance-specific and global middlewares are applied to the same state, instance-specific middlewares will run first.)
If you need middleware to apply only within a specific component, you can pass it directly when using
useSharedState
.
import { useSharedState } from 'overwatch-ts';
const MyComponentWithLocalLogging = () => {
const localLogger = (val, next) => {
console.log('This runs only in this specific component:', val);
next(val);
};
const [theme, setTheme] = useSharedState('theme', {
middleware: [localLogger],
});
// ...rest of your component
};
These hooks are designed for one-time communications, such as triggering a logout action, showing a modal, or sending a specific notification across your application. While distinct from state management, they integrate seamlessly for a comprehensive communication solution.
Broadcast an event:
import { useBroadcast } from 'overwatch-ts';
const LogoutButton = () => {
const broadcast = useBroadcast();
const handleLogout = () => {
// Perform logout logic...
broadcast('LOGOUT'); // Emit the 'LOGOUT' event
};
return <button onClick={handleLogout}>Logout</button>;
};
Listen for an event:
import { useEvent } from 'overwatch-ts';
import { useNavigate } from 'react-router-dom'; // Example for React Router
const AuthListener = () => {
const navigate = useNavigate();
useEvent('LOGOUT', () => {
console.log('Logout event received. Redirecting...');
navigate('/login');
});
return null; // This component primarily serves as a listener
};
In many applications, especially those with deep component trees or when React Context feels too rigid, there's a need for cross-component communication that's both decoupled and clear. OverWatch's event system addresses this by:
- Offering a central event bus built on the pub-sub model.
- Leveraging custom React hooks for subscribing to and publishing events.
- Automatically unsubscribing events when components unmount, preventing memory leaks.
- Tracking which component subscribed to which event for better debugging.
- Being fully written in TypeScript and easily tree-shakable for efficient bundling.
- Decoupled communication between unrelated components.
- Managing temporary global states (e.g., flash messages, user actions).
- Providing an alternative to prop drilling or over-reliance on React Context.
Overwatch supports SSR (Server Side Rendering) via a dedicated ServerStore
interface, giving you full control over how shared state is created, serialized, and hydrated across the client-server boundary.
How to use (β¨Easy!)
- On the server, create a new store instance using createServerStore().
- Set and read state via that store during rendering.
- On the client, hydrate the store using Hyrated wrapper before your app renders.
That's it 3 steps
Use createServerStore()
to get a per-request store instance:
const serverStore = createServerStore();
Use serverStore.getSnapshot()
to pass it to the client, and serverStore.hydrate()
or useHydratedStore
on the client to Hydrate client-side stores with no flickering.
// Server: Express/Node
import { createServerStore } from 'overwatch-ts';
const serverStore = createServerStore();
serverStore.set('theme', 'dark'); // Inject initial state
const app = renderToString(
<App store={serverStore} />
);
// Send snapshot to client
const initialState = serverStore.getSnapshot();
res.send(`
<script>window.__OVERWATCH_SNAPSHOT__ = ${JSON.stringify(initialState)}</script>
`);
// Client: Hydrate snapshot
import { useHydratedStore } from 'overwatch-ts';
if (typeof window !== 'undefined' && window.__OVERWATCH_SNAPSHOT__) {
useHydratedStore(window.__OVERWATCH_SNAPSHOT__);
}
'use client'
import { createServerStore } from 'overwatch-ts';
export const serverStore = createServerStore();
import { Hydrated } from 'overwatch-ts';
export async function getServerSideProps() {
// Set some state server-side
serverStore.set('theme', { mode: 'dark' });
return {
props: {
initialSnapshot: serverStore.getSnapshot(),
},
};
}
export default function Home({ initialSnapshot }) {
return (
// Wrapper that renders children only after client is fully hydrated
<Hydrated snapshot={initialSnapshot}>
<ToogleTheme /> // client *components
</Hydrated>
);
}
On the client
"use client"
export const ToogleTheme = () => {
const [theme, setTheme] = useSharedState('theme');
return (<>
<h1>{theme.mode}</h1>
</>)
}
- Use
createServerStore()
to create scoped state per request. - Populate initial state using
createSharedState(...)
. - Use
store.getSnapshot()
and pass it to the client. - Hydrate on the client using
Hydrated
wrapper before using hooks. - All hooks internally default to the global store unless a
store
is passed manually. (Not Recommended, only for advance use cases)
MIT β feel free to fork and adapt OverWatch for your projects.
Built with purpose, and reusability in mind.