π€― Simple, reactive, tiny and performant state-management library for React (just 3kb)
π Documentation website
- βοΈ Reactive with store object changes (nested properties too π!!)
- β Simple & minimalistic API
- π Batched updates and optimized re-renders
- π¨ Lazy listen nested properties
- π Immutable changes
- π Typescript support
# NPM
npm install @cervello/react
# PNPM
pnpm add @cervello/react
# YARN
yarn add @cervello/reactIt's as simple as reassign a new value to the store properties.
It will notify all the components using useStore hook to re-render with the new value.
// - store-example.ts
import { cervello } from '@cervello/react'
export const {
store, // Object with reactive changes
useStore, // Hook to listen for store or partial store changes
reset, // Function to reset the store to initial value
} = cervello({
fullName: 'Cervello Store',
address: {
city: 'Huelva',
/* ... */
},
})
// Change value from anywhere
store.address.city = 'Sevilla'
// Listen for changes from components
function Address() {
const { address } = useStore()
return (<p>City: {address.city}</p>)
}
// Just listen for changes in the `city` property
const AddressWithSelector = () => {
const { address } = useStore({
select: ['address.city']
})
return (<p>City: {address.city}</p>)
}- Batched changes: mutations are coalesced and flushed once per microtask.
onChange/afterChangereceive the whole batch as an array (don't mutate it β it may be shared between subscribers). - No spurious updates: assigning content-equal values (same primitive, or a plain object with the same content β key order doesn't matter) does not notify or re-render. Arrays,
nonReactiveobjects and React elements always notify. A component that already rendered after a write (e.g. a handler that callssetStateand then writes to the store) is not re-rendered again by that write. - Nested objects are live views:
const { address } = useStore()(orconst a = store.address) keeps pointing at the store's current data even afterstore.address = {...},store.$value = {...}orreset(), so writing through it always updates the store. If its path stops being an object (store.address = null), writes through the old handle are dropped. $valueis always a copy: readingstore.$valuereturns a deep clone, andstore.$value = obj(likecervello(obj)andreset()) stores a deep clone ofobjβ mutatingobjafterwards does not touch the store. Prefer{ ...store.$value, x }over{ ...store, x }when building a new value.- Hot components: pass
select: ['path', 'nested.*']touseStoreso the component only re-renders for those paths; without it, it re-renders on every store change. Exact paths match their own writes;'address.*'also matches anything nested underaddress. When an ancestor is reassigned (store.address = {...},store.$value = {...}orreset()), the selected slice is compared by content and the component only re-renders if it actually changed. Rule: a component must only read the paths it selects β a non-selected field read in render is never refreshed. afterChangefires on every emission: field writes, whole-store replacement (store.$value = ...,reset()) andinitialValueseeds β content-equal writes never emit, so they never fire it either. For seeds it is deferred to a microtask (never inside a React render). A subscriber (onChange) that throws does not stop the other subscribers nor later updates: the error is re-thrown asynchronously.- StrictMode:
initialValueis render-phase code (double-invoked in dev β keep it idempotent) andsetValueOnMountruns on each effect mount, per React's contract. - SSR: the store is module-scoped. For per-request isolation, call
cervello()per request and share it via context. - Complex values (
Date,Map,Set, class instances, circular refs): wrap them withnonReactive(...)β they are kept intact (and restored byreset()) but don't trigger reactivity.
To see more in depth explanations or API references and more examples: π Documentation website
Created with Typescript! β‘ and latin music πΊπ΅