react-dollar-store v0.5.5
State management library based on atoms inspired by Recoil.js and Jotai.
- No store keys required
- State lives outside of the React Tree
- Scoped stores are supported through a higher level API, see
contextualStore
- Works with Suspense and Transitions
- Smooth integration with SSR
- Strongly typed
Getting Started
store
Stores can hold any value.
Usage
const $message = store("hello world!");
const $counter = store(1);
const $list = store([1, 2, 3]);
const $promise = store(fetchImportantData());
const $person1 = store<Person>({
name: "John",
age: 32,
});
const $person2 = store<Person>(); // Without initial value, return type is `Person | undefined`
// Alternatively, you can destructure the store like an array to extract the setter and leaving a readonly store.
// This is useful when you don't want to expose the setter or when you only want to expose your own mutatation functions.
const [$readOnlyStore, setter] = store(false);
// $readOnlyStore.set(true)
// > Uncaught TypeError: $readOnlyStore.set is not a function
The store
method supports a function to initialize the store. This is useful when you want to defer the initialization of the store until its used.
const $promise = store(() => fetchCurrentUser());
Reference
function store<T>(): Store<T | undefined>;
function store<T>(value: T): Store<T>;
function store<T>(value: () => T): Store<T>;
interface Store<T> {
get(): T;
set(value: T | ((value: T) => T)): void;
subscribe(subscriber: () => void): () => void; // Returns an unsubscribe function
}
effect
Executes a function when one of the dependency stores changes.
You can return a clean up function inside the effect.
The effect
call returns a function that you can call to cancel the effect.
Usage
In the following example we will use an effect to keep a Post in sync with the current Post Id.
type Post = {
id: number;
title: string;
body: string;
};
const $postId = store(1);
const $post = store<Promise<Post>>();
// Update $post everytime $postId changes
const cancelEffect = effect(
// This api is not completely similar to React's useEffect.
// Here for example, you get the value of your dependencies as arguments of your effect function.
(postId) => $post.set(fetchPost(postId)),
[$postId]
);
Reference
type VoidFunction = () => void;
type CleanUp = VoidFunction;
type CancelEffect = VoidFunction;
function effect<T extends ReadonlyStore<unknown>[]>(
effect: (...storeValues: InferStoreListOutput<T>) => void | CleanUp,
dependencyStores: [...T]
): CancelEffect;
compute
Creates a derived a value from other stores. The value is recomputed whenever any of the dependencies change. Computed stores don't have a setter.
Usage
const $letters = store(["a", "b", "c"]);
const $numbers = store([1, 2, 3]);
const $itemsCount = compute(
(letters, numbers) => letters.length + numbers.length,
[$letters, $numbers]
);
console.log($itemsCount.get()); // 6
$numbers.set((currentValue) => [...currentValue, 4, 5]);
console.log($itemsCount.get()); // 8
Reference
function compute<T, U extends ReadonlyStore<unknown>[]>(
evaluationFn: (...storeValues: InferStoreListOutput<U>) => T,
dependencyStores: [...U]
): ReadonlyStore<T>;
contextualStore
The store
API is not a replacement of React's Context API.
React Context not only allows you to share a value between components, but it also does this under different scopes (Context Provider).
You can achieve the same effect by creating a Context and passing a store as value to the provider, but in order to reduce some boilerplate we provide a utility to do this for you.
Keep in mind that since this store depends on the context, you won't be able to access its value from outside the React tree like the regular store.
Usage
const [CountContextProvider, useStoreInstance] = contextualStore(0);
function Count() {
const $counter = useStoreInstance(); // Get store from context
const count = useStoreValue($counter); // Read store value
return <span>{count}</span>;
}
function Counter() {
const $counter = useStoreInstance(); // Will never trigger re-renders
return (
<div>
<button onClick={() => $counter.set((v) => v - 1)}>-</button>
<button onClick={() => $counter.set((v) => v + 1)}>+</button>
</div>
);
}
function App() {
return (
<>
<CountContextProvider>
<div>
<Count />
<Counter />
</div>
</CountContextProvider>
{/* Override initial value */}
<CountContextProvider initialValue={1000}>
<div>
<Count />
<Counter />
</div>
</CountContextProvider>
</>
);
}
Reference
function contextualStore<T>(): readonly [
StoreProviderWithoutInitial<T>,
() => Store<T | undefined>
];
function contextualStore<T>(initialValue: T): readonly [StoreProvider<T>, () => Store<T>];
Hooks
useStore
Subscribes to a store. It returns a tuple where the first item is the store value and the second item is a function to update it.
Usage
function Count() {
const [counter, setCounter] = useStore($counter);
return (
<div>
<span>{counter}</span>
<button onClick={() => setCounter((v) => v + 1)}>Increment</button>
</div>
);
}
Reference
function useStore<T>(store: Store<T>): readonly [T, (value: T) => void];
// Sets the initial value of the store in case it was created without one
// This also removes `undefined` from `Store<T | undefined>` making it safe to read.
function useStore<T>(
store: Store<T | undefined>,
initialValue: T
): readonly [T, (value: T) => void];
function useStore<T, TSelection>(
store: Store<T>,
selector: (state: T) => TSelection
): readonly [TSelection, (value: T) => void];
function useStore<T, TSelection>(
store: Store<T>,
initialValue: T,
selector: (state: T) => TSelection
): readonly [TSelection, (value: T) => void];
useStoreValue
Subscribes to a store and returns the store value.
Reference
Similar to useStore
.
useProxyStore
Returns a proxy object that will update the store when you mutate it. Only works with object stores.
const $counter = store({ value: 0 });
function Example() {
const counter = useProxyStore($counter);
return (
<div>
<div>{counter.value}</div>
<button
onClick={() => {
counter.value += 1;
}}
>
+
</button>
</div>
);
}
useAwait
/ useAwaitResult
Reads the promise result. Suspends the component until the stored promise is either resolved or rejected.
Throws an error if the store does not contain a promise.
type Post = {
id: number;
title: string;
body: string;
};
const $post = store(fetchPost({ id: 1 }));
function Post() {
const post = useAwaitResult($post); // Component will suspend until the promise resolves
return (
<div>
<div>{post.title}</div>
<div>{post.body}</div>
</div>
);
}
In some cases, its not possible to create a store with an initial promise.
To prevent the hook from throwing an error, we set the promise inside an event handler on the parent component and before rendering the component that suspends.
type Post = {
id: number;
title: string;
body: string;
};
const $post = store<Post>(); // No initial value. Type of $post is `Store<Post | undefined>`
function Post() {
const post = useAwaitResult($post);
return (
<div>
<div>{post.title}</div>
<div>{post.body}</div>
</div>
);
}
function App() {
const [postId, setPostId] = useState(1);
const [postPromise, setPostPromise] = useStore($post); // `useStore` does not suspend.
const handleNextPostClick = () => {
const newPostId = postId + 1;
setPostId(newPostId);
setPostPromise(fetchPost({ id: newPostId })); // <- Set the promise here
};
return (
<div>
<button onClick={handleNextPostClick}>Next Post</button>
{/* `postPromise` is undefined on the first render (therefore $post is empty) so we need to prevent <Post /> from rendering. */}
{postPromise && (
<Suspense fallback={"Loading..."}>
<Post />
</Suspense>
)}
</div>
);
}
You might not need React Query! 🤯
usePromiseStore
Reads the promise from the store, unwraps it, and returns its state. Does not suspend the component.
Usage
const $todoPromise = store(fetchTodo({ id: 1 }));
function Todo() {
const todo = usePromiseStore($todoPromise);
if (todo.error) {
return <div>An error ocurred</div>;
} else if (!todo.data) {
return <div>Loading...</div>;
}
return <div>{todo.data.title}</div>;
}
Reference
type UnwrappedPromise<T> = {
isPending: boolean;
data?: T;
error?: unknown;
};
function usePromiseStore<T>(store: ReadonlyStore<Promise<T>>): UnwrappedPromise<T>;
Server Side Rendering (SSR)
IMPORTANT THINGS TO KNOW:
If a store is read during a server side render, its value must be serializable. This is because we have to send the value over the network in order to hydrate the stores in the client successfully.
In SSR frameworks, you might want to use the function syntax for stores that have a promise as initial value to prevent the promise from start executing when just importing the module. This will defer the execution of the promise until the store is read.
NextJS
In Pages Router, wrap your app in <HydrationBoundary />
.
function App() {
return (
<HydrationBoundary>
<Component1 />
<Component2 />
<Component3 />
</HydrationBoundary>
);
}
For the App Router the setup is slightly harder since we depend on Next's useServerInsertedHTML
hook.
"use client"
import { $RDSCache, StoreContent, useHydrateCache } from "react-dollar-store"
function Provider() {
const RDS_SCRIPT_ID = "__RDS_CACHE__" // This value can be whatever you want
// This will hydrate the RDS cache on the client.
useHydrateCache(RDS_SCRIPT_ID)
// This will serialize the RDS cache on the server and place it in a script tag in the HTML document.
useServerInsertedHTML(() => {
return <StoreContent $store={$RDSCache} id={RDS_SCRIPT_ID} />
})
}
// Instead of this:
const $promise = store(fetchUserInfo());
// Do this:
const $promise = store(() => fetchUserInfo());
Additional Utilities
draft
Takes a setter function that mutates the state, and returns a setter function that creates a new state. Similar to immer. Can be used with React.useState
.
const $person = store({
name: "John",
age: 30,
});
$person.set(
draft((state) => {
state.name = "Jane";
})
);
enhanceSetter
Takes a store setter function, and returns a new setter that supports mutations. Similar to draft
but saves you from a lot of boilerplate when you need to mutate a store multiple times.
const $person = store({
name: "John",
age: 30,
});
const mutatableSetter = enhanceSetter($person.set);
mutatableSetter((state) => {
state.name = "Jane";
});
proxify
Takes a store setter function, and returns a new setter that supports mutations. Similar to draft
but saves you from a lot of boilerplate when you need to mutate a store multiple times.
const $person = store({
name: "John",
age: 30,
});
const proxy = proxify($person.get, $person.set);
proxy.name = "Jane" // Same as $person.set(v => ({ ...v, name: "Jane" })
Also works with React's useState:
const [state, setState] = useState({ name: "John", age: 30 });
const proxy = proxify(() => state, setState);
proxy.name = "Jane" // Same as setState(v => ({ ...v, name: "Jane" })
useProxyState
This hooks does not require a store. Its just a wrapper around useState
that returns a proxy object that updates the state when you mutate it.
usePromiseState
This hooks does not require a store. It unwraps a promise and returns its state. The promise must be memoized outside of the component that uses this hook, otherwise the hook will fall in an infinite loop.
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
8 months ago
10 months ago
10 months ago
1 year ago
12 months ago
12 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago