Skip to main content

Contection

A state management library that extends React Context API with fine-grained subscriptions and computed values. Built on React hooks and useSyncExternalStore to provide efficient, granular state updates while maintaining a React-native API.

Features 

Installation 

npm install contection
# or
yarn add contection
# or
pnpm add contection

Quick Start 

1. Create a Store 

import { createStore } from "contection";

interface AppStoreType {
user: { name: string; email: string };
count: number;
theme: "light" | "dark";
}

const AppStore = createStore<AppStoreType>({
user: { name: "", email: "" },
count: 0,
theme: "light",
});

2. Provide the Store 

Each Provider instance creates its own isolated store scope. Components within a Provider can only access the store state from that Provider's scope, similar to React Context.Provider:

function App() {
return (
{/* same as AppStore.Provider */}
<AppStore>
<YourComponents />
</AppStore>
);
}

Multiple Providers create separate scopes:

function App() {
return (
<>
{/* First scope with initial data */}
<AppStore
value={{
user: { name: "Alice", email: "alice@example.com" },
count: 0,
theme: "light",
}}
>
<ComponentA />
</AppStore>

{/* Second scope with different initial data - completely isolated */}
<AppStore
value={{
user: { name: "Bob", email: "bob@example.com" },
count: 10,
theme: "dark",
}}
>
<ComponentB />
</AppStore>
</>
);
}

3. Use the Store 

Using Hooks (Recommended)

import { useStore } from "contection";

function Counter() {
const { count } = useStore(AppStore, { keys: ["count"] });

return (
<div>
<p>Count: {count}</p>
<button
onClick={() => {
// Access store via useStoreReducer for updates
}}
>
Increment
</button>
</div>
);
}

Using Consumer Component

function UserProfile() {
return (
<AppStore.Consumer options={{ keys: ["user"] }}>
{({ user }) => (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)}
</AppStore.Consumer>
);
}

Advanced Usage 

Updating the Store 

Use useStoreReducer to get the store state and dispatch function. Unlike useStore, the store returned from useStoreReducer does not trigger re-renders when it changes, making it useful for reading values without subscribing to updates:

import { useStoreReducer } from "contection";

function Counter() {
const [store, dispatch] = useStoreReducer(AppStore);

return (
<div>
<button onClick={() => alert(store.count)}>Show count</button>
<button onClick={() => dispatch({ count: store.count + 1 })}>
Increment
</button>
<button onClick={() => dispatch((prev) => ({ count: prev.count - 1 }))}>
Decrement
</button>
</div>
);
}

Selective Subscriptions 

Subscribe to specific store keys to limit re-render scope:

// Component re-renders only when 'count' key changes
const { count } = useStore(AppStore, { keys: ["count"] });

// Component re-renders only when 'user' or 'theme' keys change
const data = useStore(AppStore, { keys: ["user", "theme"] });

Computed Values 

Derive computed state from store values using mutation functions:

// Compute derived value from store state
// Component re-renders only when mutation result change
const userInitials = useStore(AppStore, {
keys: ["user"],
mutation: (user) => {
const names = user.name.split(" ");
return names
.map((n) => n[0])
.join("")
.toUpperCase();
},
});

// Returns computed string value (e.g., "JD") instead of the full user object

Full Store Access 

Access the entire store when needed with full re-render cycle:

// Get the entire store
const store = useStore(AppStore);

// Or with Consumer
<AppStore.Consumer>
{(store) => (
<div>
<p>User: {store.user.name}</p>
<p>Count: {store.count}</p>
<p>Theme: {store.theme}</p>
</div>
)}
</AppStore.Consumer>;

Imperative Subscriptions 

Use listen and unlisten for imperative subscriptions outside React's render cycle. Useful for side effects, logging, or external system integrations:

import { useStoreReducer } from "contection";
import { useEffect } from "react";

function AnalyticsTracker() {
const [store, dispatch, listen, unlisten] = useStoreReducer(AppStore);

useEffect(() => {
const unsubscribeUser = listen("user", (user) => {
analytics.track("user_updated", { userId: user.email });
});

const unsubscribeTheme = listen("theme", (theme) => {
document.documentElement.setAttribute("data-theme", theme);
});

// Cleanup subscriptions on unmount
return () => {
unsubscribeUser();
unsubscribeTheme();
};
}, [listen]);

return null;
}

Manual subscription management with unlisten:

import { useStoreReducer } from "contection";
import { useRef } from "react";

function CustomSubscription() {
const [store, dispatch, listen, unlisten] = useStoreReducer(AppStore);
const listenerRef = useRef<((value: number) => void) | null>(null);

const startTracking = () => {
const listener = (count: number) => {
console.log(`Count changed to: ${count}`);
if (count > 10) {
alert("Count exceeded 10!");
}
};

listenerRef.current = listener;
listen("count", listener);
};

const stopTracking = () => {
if (listenerRef.current) {
unlisten("count", listenerRef.current);
listenerRef.current = null;
}
};

return (
<div>
<button onClick={startTracking}>Start Tracking</button>
<button onClick={stopTracking}>Stop Tracking</button>
</div>
);
}

Using the returned unsubscribe function:

import { useStoreReducer } from "contection";
import { useEffect } from "react";

function AutoCleanupSubscription() {
const [, , listen] = useStoreReducer(AppStore);

useEffect(() => {
const unsubscribe = listen("count", (count) => {
console.log("Current count:", count);
});

return unsubscribe;
}, [listen]);
}

Lifecycle Hooks 

Lifecycle hooks allow you to perform initialization and cleanup operations at different stages of the store's lifecycle. They are passed as options to createStore:

const AppStore = createStore<AppStoreType>(
{
user: { name: "", email: "" },
count: 0,
theme: "light",
},
{
lifecycleHooks: {
storeWillMount: (store, update, listen, unlisten) => {
// Initialization logic
// Return cleanup function if needed
},
storeDidMount: (store, update, listen, unlisten) => {
// Post-mount logic
// Return cleanup function if needed
},
storeWillUnmount: (store) => {
// Synchronous cleanup before unmount
},
storeWillUnmountAsync: (store) => {
// Asynchronous cleanup during unmount
},
},
}
);

storeWillMount

Recommended for: Single Page Applications (SPA), background key detection or subscriptions.

Runs synchronously during render, before the store is fully initialized. This hook is ideal for:

Important: In React Strict Mode (development), storeWillMount is called twice. Return a cleanup function to properly handle subscriptions and prevent memory leaks:

const AppStore = createStore<AppStoreType>(
{
user: { name: "", email: "" },
count: 0,
theme: "light",
lastVisit: null as Date | null,
},
{
lifecycleHooks: {
storeWillMount: (store, update, listen) => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme) {
update({ theme: savedTheme as "light" | "dark" });
}
const unlisten = listen("count", (count) => {
console.log("Count changed:", count);
});
return unlisten;
},
},
}
);

storeDidMount

Recommended for: Fullstack frameworks (Next.js, Remix, etc.) to avoid hydration errors.

Runs asynchronously after the component mounts, making it safe for operations that might differ between server and client. This hook is ideal for:

const AppStore = createStore<AppStoreType>(
{
user: { name: "", email: "" },
count: 0,
theme: "light",
windowWidth: 0,
},
{
lifecycleHooks: {
storeDidMount: (store, update, listen, unlisten) => {
update({ windowWidth: window.innerWidth });

const handleResize = () => {
update({ windowWidth: window.innerWidth });
};
window.addEventListener("resize", handleResize);

// Return cleanup function
return () => {
window.removeEventListener("resize", handleResize);
};
},
},
}
);

storeWillUnmount

Recommended for: Synchronous cleanup operations that must complete before the component unmounts.

Runs synchronously in useLayoutEffect cleanup, before the component unmounts. This hook is ideal for:

Note: This hook runs synchronously and should not perform heavy operations that could block the UI.

const AppStore = createStore<AppStoreType>(
{
user: { name: "", email: "" },
count: 0,
theme: "light",
},
{
lifecycleHooks: {
storeWillUnmount: (store) => {
if (store.count > 0) {
localStorage.setItem("lastCount", String(store.count));
}
},
},
}
);

storeWillUnmountAsync

Recommended for: Asynchronous cleanup operations that can run during unmount.

Runs asynchronously in useEffect cleanup, during component unmount. This hook is ideal for:

Execution Order: This hook runs after storeDidMount cleanup (if provided) and after storeWillMount cleanup (if provided).

const AppStore = createStore<AppStoreType>(
{
user: { name: "", email: "" },
count: 0,
theme: "light",
},
{
lifecycleHooks: {
storeDidMount: (store, update, listen, unlisten) => {
const ws = new WebSocket("wss://example.com");

return () => {
ws.close();
};
},
storeWillUnmountAsync: (store) => {
fetch("https://example.com/api/sync-state", {
method: "POST",
body: JSON.stringify(store),
}).catch(console.error);
},
},
}
);

Lifecycle Execution Order 

  1. Mount Phase:
  1. Unmount Phase:

API Reference 

createStore(initialData: Store, options?) 

Creates a new store instance with Provider and Consumer components.

Parameters:

Returns:

useStore(instance, options?) 

Hook that subscribes to store state with optional key filtering and computed value derivation.

Parameters:

Returns: Subscribed store data or computed value if mutation function is provided

useStoreReducer(instance) 

Hook that returns a tuple containing the store state and dispatch functions, similar to useReducer.

Returns: [store, dispatch, listen, unlisten] tuple where:

Provider 

Component that provides a scoped store instance to child components. Each Provider instance creates its own isolated store scope, similar to React Context.Provider. Components within a Provider can only access the store state from that Provider's scope.

Props:

Scoping Behavior:

Consumer 

Component that consumes the store using render props pattern.

Props:

Architecture 

Contection addresses limitations of the standard React Context API:

  1. Granular Updates - Implements useSyncExternalStore to enable per-key subscriptions, preventing unnecessary re-renders when unrelated state changes
  2. Selective Subscriptions - Components subscribe only to specified store keys, reducing render cycles
  3. Computed State - Built-in support for derived state through mutation functions
  4. React Patterns - Maintains compatibility with standard React hooks and component patterns
  5. Type Safety - TypeScript generics provide compile-time type checking for store keys and computed values

License 

MIC

Last modified on
Previous
Top Layer
Next
Path Parser
Return to navigation