npm i con-estado
yarn add con-estado
deno add jsr:@rafde/con-estado
con-estado
is a state management library built on top of Mutative,
like Immer but faster,
with the goal of helping with deeply nested state management.
With TypeScript support, strongly infers as much as it can for state and callback functions so you can use type-safe selectors and actions.
Managing deeply nested state in React often becomes cumbersome with traditional state management solutions. con-estado
provides:
- Direct path updates: Modify nested properties using dot-notation or
Array<string | number>
instead of spreading multiple levels - Referential stability: Only modified portions of state create new references, preventing unnecessary re-renders
- Custom selectors: Prevent component re-renders by selecting only relevant state fragments
- Type-safe mutations: Full TypeScript support for state paths and updates
Built on Mutative's efficient immutable updates, con-estado
is particularly useful for applications with:
- Complex nested state structures
- Performance-sensitive state operations
- Frequent partial state updates
- Teams wanting to reduce state management boilerplate
Key advantages:
- Optimized subscriptions through selector-based consumption
For applications needing global state management, createConStore
provides a solution for creating actions and optimized updates:
Key advantages:
- Global state accessible across components
- Optimized subscriptions through selector-based consumption
Selector is a function
that returns the props you need. Only re-renders on non-function
changes.
Optimize renders by selecting only needed state:
function UserPreferences() {
const preferences = useCon( initialState, props => ( {
theme: props.state.user.preferences.theme,
updateTheme( event: ChangeEvent<HTMLSelectElement> ) {
props.set(
event.target.name as Parameter<typeof props.set>[0],
event.target.value,
);
},
} ),
);
return <select
value={preferences.theme}
name="user.preferences.theme"
onChange={preferences.updateTheme}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>;
}
Define reusable actions for complex state updates:
function PostList() {
const [ state, { acts, } ] = useCon(
{ posts: [ { id: 1, text: 'post', } ] },
{
acts: ( { currySet, wrapSet, } ) => {
// currySet is a function that
// returns a function that can be called with the posts array
const setPost = currySet('posts');
return {
addPost( post: Post, ) {
setPost( ( { draft } ) => {
draft.push( post );
});
},
updatePost: wrapSet(
'posts',
( { draft }, id: number, updates: Partial<Post>, ) => {
const post = draft.find( p => p.id === id );
if (post) Object.assign( post, updates );
}
),
async fetchPosts() {
const posts = await api.getPosts();
setPost( posts );
},
},
},
}
);
return <div>
{state.posts.map( post => (
<PostItem
key={post.id}
post={post}
onUpdate={updates => acts.updatePost(post.id, updates)}
/>
) )}
<button onClick={acts.fetchPosts}>
Refresh Posts
</button>
</div>;
}
Track and access previous state values:
- state: Current immutable state object.
- prev: The previous
state
immutable object beforestate
was updated. - initial: Immutable initial state it started as. It can be updated through
historyDraft
for resync purposes like merging with server data whilestate
keeps client side data. - prevInitial: The previous
initial
immutable object beforeinitial
was updated. - changes: Immutable object that keeps track of top level properties (shallow) difference between the
state
andinitial
object.
function StateHistory() {
const [ state, { get, reset, }, ] = useCon( initialState, );
const history = get(); // Get full state history
const prev = history.prev;
return <div>
<pre>{JSON.stringify(prev, null, 2)}</pre>
<button onClick={reset}>Reset State</button>
</div>;
}
createConStore
and useCon
take the same parameters.
// works with createConStore
useCon( {} );
useCon( () => ({}) );
useCon( [] );
useCon( () => ([]) );
Used to initialize the state
value. non-null Object
with data, Array
,
or a function that returns an Object
or Array
Configuration options for createConStore
and useCon
.
useCon( initial, options );
Callback function
for creating a Record of action handlers. The action handlers have access to a subset of the controls object.
useCon(
initial,
{
acts: ( {
set,
currySet,
setWrap,
get,
reset,
getDraft,
setHistory,
currySetHistory,
setHistoryWrap,
}: ActControls ) => ( {
// your actions with async support
yourAction( props, ) {
// your code
}
} ),
}
);
Async callback after state changes.
useCon(
initial,
{
afterChange(
{ state, initial, prev, prevInitial, changes, }: History
) {
// your code with async support
}
}
);
Callback function
to transform the state
and/or initial
properties before it is set/reset. Receives a draft and current history
useCon(
initial,
{
transform: (
{ state, initial, }: HistoryDraft,
{ state, initial, prev, prevInitial, changes, }: History,
type: 'set' | 'reset',
) => {
// your code
}
}
);
Configuration for mutative
options.
{enablePatches: true}
not supported.
Custom selector
callback that lets you shape what is returned from useCon
and createConStore
.
useCon
Example:
useCon(
initialState,
options,
( {
state,
acts,
set,
currySet,
setWrap,
get,
reset,
getDraft,
setHistory,
currySetHistory,
setHistoryWrap,
useSelector, // only available in `useCon`
subscribe,
}: UseConControls, ) => unknown // unknown represents the return type of your choice
);
// Example without options
useCon( initialState, selector, );
createConStore(
initialState,
options,
( {
state,
acts,
set,
currySet,
setWrap,
get,
reset,
getDraft,
setHistory,
currySetHistory,
setHistoryWrap,
subscribe,
}: CreateConStoreControls, ) => unknown // unknown represents the return type of your choice
);
// Example without options
createConStore( initialState, selector, );
TIP: When selectors return a function or object with a function, those functions will not trigger re-render when changed. This is a precaution to prevent unnecessary re-renders since creating functions create a new reference.
Examples:
// Won't re-render
const setCount = useCon(
initialState,
controls => controls.state.count < 10
? controls.setWrap('count')
: () => {}
);
// Won't re-render, but it will do something.
const setCount = useCon( initialState, controls => (value) => {
controls.state.count < 10
? controls.set('count', value)
: undefined
});
// This will re-render when `controls.state.count` has updated.
const setCount = useCon( initialState, controls => ({
count: controls.state.count,
setCount: controls.state.count < 10
? controls.setWrap('count')
: () => {}
}));
const useConSelector = createConStore(
initialState,
( { set, }, ) => set,
);
// this will never trigger re-renders because the selector returned a function.
const set = useConSelector();
// this will re-render when `state` changes.
const [
set,
state,
] = useConSelector( ( { set, state, }, ) => [ set, state, ] as const );
Local state manager for a React Component
const [ state, controls, ] = useCon( initialState, options, selector, );
useCon
has access to additional control property from selector
named useSelector
. A function
that works like what createConStore
returns.
- By default, returns
[state, controls]
when no selector is provided. If aselector
is provided, it returns the result of theselector
. - This allows you to use local state as a local store that can be passed down to other components, where each component can provide a custom
selector
.
const useSelector = useCon( initialState, controls => controls.useSelector );
const state = useSelector(controls => controls.state);
const set = useSelector(controls => controls.set);
TIP: If your selector
return value is/has a function
, function will not be seen as a change to
trigger re-render. This is a precaution to prevent unnecessary re-renders since all dynamic functions create a new reference.
If you need to conditional return a function
, it's better if you make a function
that can handle your condition.
example
// Won't re-render
const setCount = useCon(
initialState,
controls => controls.state.count < 10
? controls.setWrap('count')
: () => {}
);
// Won't re-render, but it will do something.
const setCount = useCon( initialState, controls => (value) => {
controls.state.count < 10 ? controls.set('count', value) : undefined
});
// This will re-render when `controls.state.count` value is updated
const setCount = useCon( initialState, controls => ({
count: controls.state.count,
setCount: controls.state.count < 10 ? controls.setWrap('count') : () => {}
}));
Global store state manager.
const useConSelector = createConStore( initialState, options, selector, );
Called useConSelector
for reference. You have a choice in naming.
By default, returns [ state, controls, ]
when no selector
is provided.
const [ state, controls, ] = useConSelector();
useConSelector
has static props
// static props
const {
acts,
set,
currySet,
setWrap,
get,
reset,
setHistory,
currySetHistory,
setHistoryWrap,
subscribe,
}: UseConSelectorControls = useConSelector
If a selector
is provided from createConStore
or useConSelector
, it returns the result of the selector
.
const yourSelection = useConSelector(
( {
state,
acts,
set,
currySet,
setWrap,
get,
reset,
setHistory,
currySetHistory,
setHistoryWrap,
subscribe,
}, ) => unknown
);
The following function
s
have access to the following controls:
Gives you immutable access to State History.
const [
state,
{ get, }
] = useCon( { count: 0, }, );
const {
get,
} = useConSelector( ( { get, } ) => ( { get, } ), ) ;
const history = get();
history.state;
history.initial;
history.changes;
history.prev;
history.prevInitial;
You can also use dot-notation to access properties.
const changesToSomeValue = get('changes.to.some.value');
The current state
value. Initialized from options.initialState.
Same value as get( 'state' )
. Provided for convenience and to trigger re-render on default selector update.
const [
state,
] = useCon( { count: 0, }, );
const {
state,
} = useConSelector(( { state, } ) => ( { state, }, ));
Updates state with either a new state object or mutation callback.
const [
state,
{ set, }
] = useCon( { count: 0, }, );
const {
set,
} = useConSelector( ( { set, } ) => ( { set, }, ));
All set
calls returns a new State History object that contains the following properties:
Updates state with a new state object.
set( { my: 'whole', data: ['items'], }, );
Updates state with a mutation callback.
Callback expects void
return type.
set( ( {
draft,
historyDraft,
state,
prev,
initial,
prevInitial,
changes,
}, ) => {
draft.value = 5;
historyDraft.initial.value = 9
}, );
Contains State History properties plus:
- draft: The mutable part of the
state
object that can be modified in the callback. - historyDraft: Mutable
state
andinitial
object that can be modified in the callback.
Specialized overload for updating state at a specified dot-notated string path with a direct value.
set( 'my.data', [ 'new', 'value', ], );
Array index number as string, example paths.0.name
= paths[0].name
.
Paths with .
(dot) in their name must be escaped, example
const initial = {
path: {
'user.name': 'Name',
},
}; // 'path.user\\.name'
Specialized overload for updating state at a specified array of strings or numbers (for arrays) path with a direct value.
Array path to the state property to update, can have dot notation, e.g. ['items', 0]
or ['users', 2, 'address.name']
Callback works the same as set( 'path.to.value', callback )
set( ['string', 'path', 0, 'to.val'], [ 'new', 'value' ] );
Specialized overload for updating state at a specified array of strings or numbers (for arrays) or dot-notated string path with a callback function.
set( 'my.data', ( {
// same as set( callback )
draft, historyDraft, state, prev, initial, prevInitial, changes,
stateProp,
prevProp,
initialProp,
prevInitialProp,
changesProp,
}, ) => {
draft.value = 5;
historyDraft.initial.value = 9
}, );
Shares the same parameters as set( callback ), in addition to:
- draft: The mutable part of the
state
value relative to path.- ALERT: When path leads to a primitive value, you must use mutate
draft
via non-destructuring.- i.e.
set( 'path.to.primitive', (props) => props.draft = 5 )
- i.e.
- ALERT: When path leads to a primitive value, you must use mutate
- stateProp: The current immutable
state
value relative to path. - initialProp: The
initial
immutable value relative to path. - prevProp: The previous immutable
state
value relative to path. Can beundefined
. - prevInitialProp: The previous immutable
initial
value relative to path. Can beundefined
. - changesProp: Immutable changed value made to the
state
value relative to path. Can beundefined
.
The acts
object contains all the available actions created from options.acts.
const [
state,
{ acts, }
] = useCon( { count: 0 } );
const {
acts,
} = useConSelector( ( { acts, } ) => ( { acts, } ), );
A convenient function
that lets you wrap set
around another function
that accepts any number of arguments
and can return any value from it.
const [
state,
{ setWrap, }
] = useCon( { count: 0, }, );
const {
setWrap,
} = useConSelector( ( { setWrap, } ) => ( { setWrap, } ), );
// Example usage
const inc = setWrap(
( { draft, }, incBy: number, ) => draft.count += incBy
);
const newInc = inc( 5, ); // returns 5
// Example usage
const incCount = setWrap(
'count',
( props, incBy: number, ) => props.draft += incBy
);
const newCount = inc( 5, ); // returns 5
The first parameter can be
- a callback
- dot-notated string, or array of string or numbers for state prop path, followed by a callback.
Creates a pre-bound set
function for a specific dot-notation string path.
Enables partial application of path for reusable state updaters
const [
state,
{ currySet, },
] = useCon( { count: 0, }, );
const {
currySet,
} = useConSelector( ( { currySet, } ) => ( { currySet, } ), );
const setCount = currySet( 'count', );
setCount( 5, );
setCount( ( props, ) => props.draft += 1 );
Works like set, but can be used to update both state
and initial
.
Works like setWrap, but can be used to update both state
and initial
.
Works like currySet, but can be used to update both state
and initial
.
Resets state
to initial
.
Returns State History with initial
set to state
values.
const [
state,
{ reset, },
] = useCon( { count: 0, }, );
const {
reset,
} = useConSelector( ( { reset, } ) => ( { reset, } ), );
reset();
Subscribes to state changes outside useSelector or useConSelector via selector.
Returns function
to unsubscribe the listener.
ALERT:When using subscribe, you have to manage when to unsubscribe the listener.
const [
state,
{ subscribe, },
] = useCon( { count: 0 }, );
const {
subscribe,
} = useConSelector( ( { subscribe, } ) => ( { subscribe, } ), );
// Subscribe to state changes
const unsubscribe = subscribe( ( { state, }, ) => {
if (state.count > 100) {
console.log( 'Why is the count so high?' );
notifyCountReached( state.count );
}
}, );
// Later, when you want to stop listening
unsubscribe();