use-one
is a lightweight state management library for React.js.
Features
- Simple hook-based API with no complex concepts
- Easy state sharing across components
- Built-in persistence capabilities for stores and hook states
- Minimal size (gzip ~2KB)
- Written in TypeScript with full type safety
npm
npm install use-one --save
pnpm
pnpm install use-one
// stores/count.ts
import { create } from 'use-one';
const initialState = { count: 0 };
const [use, store] = create(initialState);
const actions = {
use,
get state() {
return store.getState();
},
increment() {
store.setState({ count: this.state.count + 1 });
},
decrement() {
store.setState({ count: this.state.count - 1 });
},
};
export const countStore = Object.assign(actions, store);
Using the Store
// CountExample.tsx
import * as React from 'react';
import { countStore } from './stores/count';
const Counter = () => {
const [state] = countStore.use();
const { count } = state;
return (
<div>
<button onClick={countStore.increment}>+1</button>
<span>{count}</span>
<button onClick={countStore.decrement}>-1</button>
<button
onClick={() => {
setTimeout(() => {
countStore.setState({
count: countStore.state.count + 2,
});
}, 2000);
}}
>
async +2
</button>
</div>
);
};
const ShowCount = () => {
const [state] = countStore.use();
return <span>Count: {state.count}</span>;
};
export default function App() {
return (
<>
<ShowCount />
<Counter />
</>
);
}
Integrate with Immer for immutable state updates:
// stores/count.ts
import { create } from 'use-one';
import { produce } from 'immer';
const initialState = { count: 0 };
const [use, store] = create(initialState);
const computed = {
get state() {
return store.getState();
},
};
const actions = {
use,
produce(cb: (state: typeof initialState) => void) {
store.setState(produce(cb));
},
increment() {
this.produce((state) => {
state.count++;
});
},
decrement() {
this.produce((state) => {
state.count--;
});
},
};
export const countStore = Object.assign(actions, computed, store);
For React Native or Expo applications, install @react-native-async-storage/async-storage
first.
import { create, persistStore, wrapState, isClient } from 'use-one';
const initialState = wrapState({ count: 0 }); // Adds ready: false
const [use, store] = create(initialState);
if (isClient) {
persistStore<typeof initialState>(store, {
key: '@CACHE_KEY',
debounce: 100, // Optional, defaults to 100ms
transform: (state) => state, // Optional state transformer
});
}
const actions = {
use,
get state() {
return store.getState();
},
increment() {
store.setState({ count: this.state.count + 1 });
},
decrement() {
store.setState({ count: this.state.count - 1 });
},
};
export const countStore = Object.assign(actions, store);
To prevent hydration errors in SSR applications (Next.js, Remix, etc.):
// store.ts
import {
create,
persistStore,
wrapState,
onPersistReady,
} from 'use-one';
const initialState = wrapState({ count: 0 });
const [use, store] = create(initialState);
onPersistReady(() => {
persistStore<typeof initialState>(store, {
key: '@CACHE_KEY',
debounce: 100,
transform: (state) => state,
});
});
const actions = {
use,
get state() {
return store.getState();
},
increment() {
store.setState({ count: this.state.count + 1 });
},
decrement() {
store.setState({ count: this.state.count - 1 });
},
};
export const countStore = Object.assign(actions, store);
// layout.tsx
import { Provider as PersistProvider } from 'use-one';
export default function Layout({ children }: { children: React.ReactNode }) {
return <PersistProvider>{children}</PersistProvider>;
}
Persist any hook's state independently of stores:
import { useState } from 'react';
import { usePersist } from 'use-one';
export function Counter() {
const [count, setCount] = useState(0);
const [isReady, clean] = usePersist<typeof count>({
key: '@count-store-key',
getState: () => count,
setState: setCount,
});
if (!isReady) return <div>Loading...</div>;
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={clean}>Clear Cache</button>
</div>
);
}
Prevent property conflicts using StrictPropertyCheck
:
import { create, type StrictPropertyCheck } from 'use-one';
const initialState = { count: 0 };
const [use, store] = create(initialState);
const _actions = {
use,
get state() {
return store.getState();
},
increment() {
store.setState({ count: this.state.count + 1 });
},
decrement() {
store.setState({ count: this.state.count - 1 });
},
};
const actions: StrictPropertyCheck<typeof _actions> = _actions;
export const countStore = Object.assign(actions, store);
Creates a new store with the following options:
useEffect
: Boolean (default: true) - UsesuseEffect
when true,useLayoutEffect
when falsename
: String - Optional name for the store
Returns [useHook, store]
where store
provides:
getState()
: Get current statesetState(newState)
: Update stateforceUpdate()
: Force component updatessubscribe(cb: (state) => void)
: Subscribe to state changessyncState(newState)
: Update state without triggering updatesdestroy()
: Clean up store resources
Visit use-one-templates for boilerplate code generation tools, especially useful for managing multiple stores in larger applications.