@plasius/react-state
v1.2.1
Published
Tiny, testable, typesafe React Scoped Store helper.
Maintainers
Readme
@plasius/react-state
Apache-2.0. ESM + CJS builds. TypeScript types included.
Overview
@plasius/react-state provides a scoped state management solution for React applications. It allows developers to create isolated, testable, and composable stores without introducing heavy dependencies.
Key traits
- React 18/19 compatible; uses
useSyncExternalStorefor tearing-safe snapshots. - Distinct-until-changed dispatch flow to avoid needless notifications and re-renders.
- Selector subscriptions accept custom equality to prevent redundant renders when slices are unchanged.
- Scoped Provider recreates its store when
initialStatechanges so fresh trees start from fresh state.
Installation
npm install @plasius/react-stateUsage Example
Accessing the store
import { createStore } from '@plasius/react-state'
type Action =
| { type: "INCREMENT_VALUE"; payload: number }
| { type: "SET_VALUE"; payload: number }
| { type: "DECREMENT_VALUE"; payload: number };
interface State {
value: number;
}
const initialState: State = { value: 0 };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT_VALUE":
return { ...state, value: state.value + action.payload };
case "DECREMENT_VALUE":
return { ...state, value: state.value - action.payload };
case "SET_VALUE":
// Distinct-until-changed: return the SAME reference if no change,
// so listeners relying on referential equality will not fire.
if (action.payload === state.value) return state;
return { ...state, value: action.payload };
default:
return state;
}
}
const store = createStore<State, Action>(reducer, initialState);
function doSomething() {
store.dispatch({ type: "INCREMENT_VALUE", payload: 1});
}Scoped react hooks
import { createScopedStore } from '@plasius/react-state'
type Action =
| { type: "INCREMENT_VALUE"; payload: number }
| { type: "SET_VALUE"; payload: number }
| { type: "DECREMENT_VALUE"; payload: number };
interface State {
value: number;
}
const initialState: State = { value: 0 };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "INCREMENT_VALUE":
return { ...state, value: state.value + action.payload };
case "DECREMENT_VALUE":
return { ...state, value: state.value - action.payload };
case "SET_VALUE":
// Distinct-until-changed: return the SAME reference if no change,
// so listeners relying on referential equality will not fire.
if (action.payload === state.value) return state;
return { ...state, value: action.payload };
default:
return state;
}
}
const store = createStore<State, Action>(reducer, initialState);
const Counter = () => {
const state = store.useStore();
const dispatch = store.useDispatch();
return (
<div>
<button id="counter-inc" onClick={() => dispatch({ type: "inc" })}>
+
</button>
<button id="counter-dec" onClick={() => dispatch({ type: "dec" })}>
-
</button>
<input
aria-label="Counter value"
title=""
id="counter-set"
type="number"
value={state.count}
onChange={(e) =>
dispatch({ type: "set", payload: Number(e.target.value) })
}
/>
</div>
);
};
function App() {
return (<><store.Provider><Counter /></store.Provider></>);
}Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
License
This project is licensed under the terms of the Apache 2.0 license.
