Muya is simple and lightweight react state management library.
- Simplified API: Only
create
andselect
. - Batch Updates: Built-in batching ensures efficient
muya
state updates internally. - TypeScript Support: Type first.
- Lightweight: Minimal bundle size.
Install with your favorite package manager:
bun add muya@latest
npm install muya@latest
yarn add muya@latest
import { create } from 'muya';
const useCounter = create(0);
// Access in a React component
function Counter() {
const count = useCounter(); // Call state directly
return (
<div>
<button onClick={() => useCounter.set((prev) => prev + 1)}>Increment</button>
<p>Count: {count}</p>
</div>
);
}
Use select
to derive a slice of the state:
const state = create({ count: 0, value: 42 });
const countSlice = state.select((s) => s.count);
// Also async is possible, but Muya do not recommend
// It can lead to spaghetti re-renders code which is hard to maintain and debug
const asyncCountSlice = state.select(async (s) => {
const data = await fetchData();
return data.count;
});
Combine multiple states into a derived state via select
method:
import { create, select } from 'muya'
const state1 = create(1);
const state2 = create(2);
const sum = select([state1, state2], (s1, s2) => s1 + s2);
Customize equality checks to prevent unnecessary updates:
const state = create({ a: 1, b: 2 }, (prev, next) => prev.b === next.b);
// Updates only when `b` changes
state.set((prev) => ({ ...prev, a: prev.a + 1 }));
Or in select methods:
const derived = select([state1, state2], (s1, s2) => s1 + s2, (prev, next) => prev === next);
Access state directly or through useValue
hook:
Each state can be called as the hook directly
const userState = create(0);
function App() {
const user = userState(); // Directly call state
return <p>User: {user}</p>;
}
Or for convenience, there is useValue
method
import { useValue } from 'muya';
function App() {
const user = useValue(userState); // Access state via hook
return <p>User: {user}</p>;
}
For efficient re-renders, useValue
provides a slicing method.
function App() {
const count = useValue(state, (s) => s.count); // Use selector in hook
return <p>Count: {count}</p>;
}
Create a new state:
const state = create(initialValue, isEqual?);
// Methods:
state.get(); // Get current value
state.set(value); // Update value
state.listen(listener); // Subscribe to changes
state.select(selector, isEqual?); // Create derived state
state.destroy(); // Unsubscribe from changes, useful for dynamic state creation in components
state.withName(name); // Add a name for debugging, otherwise it will be auto generated number
Combine or derive new states:
const derived = select([state1, state2], (s1, s2) => s1 + s2);
// Methods:
derived.get(); // Get current value
derived.listen(listener); // Subscribe to changes
derived.select(selector, isEqual?); // Create nested derived state
derived.destroy(); // Unsubscribe from changes, useful for dynamic state creation in components
derived.withName(name); // Add a name for debugging, otherwise it will be auto generated number
React hook to access state:
const value = useValue(state, (s) => s.slice); // Optional selector
- Equality Check: Prevent unnecessary updates by passing a custom equality function to
create
orselect
. - Batch Updates: Muya batches internal updates for better performance, reducing communication overhead similarly how react do.
- Async Selectors / Derives: Muya has support for async selectors / derives, but do not recommend to use as it can lead to spaghetti re-renders code which is hard to maintain and debug, if you want so, you can or maybe you should consider using other alternatives like
Jotai
.
Muya
encourage use async updates withing sync state like this:
const state = create({ data: null });
async function update() {
const data = await fetchData();
state.set({ data });
}
But of course you can do
Note: Handling async updates for the state (set
) will cancel the previous pending promise.
const state = create(0)
const asyncState = state.select(async (s) => {
await longPromise(100)
return s + 1
})
Muya
can be used in immediate
mode or in lazy
mode. When create a state with just plain data, it will be in immediate mode, but if you create a state with a function, it will be in lazy mode. This is useful when you want to create a state that is executed only when it is accessed for the first time.
// immediate mode, so no matter what, this value is already stored in memory
const state = create(0)
// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
const state = create(() => 0)
And in async:
// we can create some initial functions like this
async function initialLoad() {
return 0
}
// immediate mode, so no matter what, this value is already stored in memory
const state = create(initialLoad)
// or
const state = create(Promise.resolve(0))
// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render
const state = create(() => Promise.resolve(0))
And when setting state when initial value is promise, set is always sync.
But as in react there are two methods how to set a state. Directly .set(2)
or with a function .set((prev) => prev + 1)
.
So how set
state will behave with async initial value?
- Directly call
.set(2)
will be sync, and will set the value to 2 (it will cancel the initial promise) - Call
.set((prev) => prev + 1)
will wait until previous promise is resolved, so previous value in set callback is always resolved.
- Values in selectors derived from another async selectors will be always sync (not promise)
- If the selector is async (
state.select(async (s) => s + 1)
) it will automatically use suspense mode, so on each change it firstly resolved promise (hit suspense) and then update the value. - If the selector is sync, but it's derived from async selector, it will also be in suspense mode - but it depends what the parent is, if the parent is async state, it will hit the suspense only once, on initial load, but if the parent is another async selector, it will hit suspense on each change.
Muya
in dev mode automatically connects to the redux
devtools extension if it is installed in the browser. For now devtool api is simple - state updates.
This library is a fun, experimental project and not a replacement for robust state management tools. For more advanced use cases, consider libraries like Zustand
, Jotai
, or Redux
.
If you enjoy Muya
, please give it a ⭐️! :)