Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ziolko/active-store
New kind of state library for React that heavily utilizes the React Suspense API.
https://github.com/ziolko/active-store
react reactjs state-management
Last synced: about 1 month ago
JSON representation
New kind of state library for React that heavily utilizes the React Suspense API.
- Host: GitHub
- URL: https://github.com/ziolko/active-store
- Owner: ziolko
- License: mit
- Created: 2023-08-31T19:29:31.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-11-14T09:13:22.000Z (about 1 month ago)
- Last Synced: 2024-11-14T09:20:53.025Z (about 1 month ago)
- Topics: react, reactjs, state-management
- Language: TypeScript
- Homepage:
- Size: 338 KB
- Stars: 18
- Watchers: 1
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
Active Store is a new state library for React that heavily utilizes the React Suspense API.
## Quick start
To get started install the npm package with `npm i active-store`. You can then create a store as shown below:
```typescript
import { createContext, useContext } from "react";
import { activeState, activeQuery, activeComputed } from "active-store";function activeAppStore() {
// activeState is the simplest building block of active store.
// It keeps a piece of state and updates UI when the state changes
// You can later access run:
// - userLogin.get() to get the current value of the active state
// - userLogin.set("new-value") to change the value stored in states
const userLogin = activeState("ziolko");// activeQuery is like useQuery in react-query - it fetches data
// for every unique set of arguments. In the case below there's only
// one argument called - login.
const githubProfile = activeQuery(
(
login: string
): Promise<{
id: number;
login: string;
avatar_url: string;
name: string;
}> =>
fetch(`https://api.github.com/users/${encodeURIComponent(login)}`).then(
(x) => x.json()
)
);// activeComputed allows to get a computed value based on
// activeState and activeQuery. It uses React Suspense api to wait
// for activeQuery to resolve
const profile = activeComputed(() => {
// get currently selected github login. You will see a lot of `.get()`
// in the databases using active-store
const login = userLogin.get();// start fetching data from activeQuery for currently
// selected github login. The line below will suspend until
// github profile finishes loading.
// There's no need for special handling of the async loading state.
return githubProfile.get(login);
});// Of course you can combine computed values in activeComputed, too
const userName = activeComputed(() => profile.get().name);return {
userLogin,
profile,
userName,
};
}const store = activeAppStore(); // Create an instance of the store
// Define a helper hook 'useStore' to get an instance
// of the store in your components
const storeContext = createContext>(store);
export const useStore = () => useContext(storeContext);
```You are now all set to use the store in your app:
```typescript
import "./App.css";
import { useStore } from "./store";
import { useActive, ActiveBoundary } from "active-store";export default function App() {
return (
{/*
Active boundary wraps a section of the app that loads and fails
together
- fallback is used while data required to load child components
is loading.
- errorFallback is used when any of the data fails to load
with an exception
*/}
);
}function CurrentUserPicker() {
const store = useStore();
const currentLogin = useActive(store.userLogin);
const options = ["stephencelis", "lidel", "arogozhnikov", "ziolko"];
return (
<>
{options.map((login) => (
store.userLogin.set(login)}
style={{ background: currentLogin === login ? "red" : "transparent" }}
>
{login}
))}
>
);
}function GithubProfile() {
const store = useStore();// React will suspend until the data is loaded
const profile = useActive(store.profile);// React will suspend until the data is loaded
const name = useActive(store.userName);return (
{name}
);
}
```## Examples
- [Simple HackerNews client](https://codesandbox.io/p/sandbox/headless-resonance-dfzgzw?file=%2Fsrc%2Fstore.ts)
- [Simple store for a feedback form with a single input](https://github.com/roombelt/timeline/blob/main/src/store/feedback.ts)
- [An example of a splitting a big store into several smaller stores](https://github.com/roombelt/timeline/blob/main/src/store/index.ts)
- [Collection of active utilities (e.g. activeLocalStorage)](https://github.com/roombelt/timeline/blob/main/src/store/utils.ts)
- [Custom utility activeDeferred acting like useDeferredValue](https://stackblitz.com/edit/vitejs-vite-f9y5fn?file=src%2FApp.tsx)## Reference
### activeState
The simplest building block of the app state. It's like the `useState` hook.
```typescript
const state = activeState(initialState: T, options?: {
// onSubscribe is called when first subscriber subscribes to the state.
// The callback returned from onSubscribe is called when last subscriber unsubscribes
onSubscribe?: () => () => void;
// Number of miliseconds the data will be cached after
// last subscriber unsubscribes (defaults to infinity)
gcTime?: number;
});// Returns the current value of the state
state.get();// Sets new value for the state. Trigers re-renders of
// components that depend on it. Triggers invalidation
// on activeComputed that depend on it.
state.set(newValue: T);// Manually subscribes to changes in the state.
// Takes a listener as a parameter. Returns unsubscribe function.
state.subscribe(listener: () => any) => () => void;
```Alternatively, you can provide a factory function as an `initialState`:
```typescript
const greetings = activeState((name: string) => `Hello ${name}`, {
onSubscribe(name: string) {
console.log(`Subscribed to ${name}`);
return () => { console.log(`Unsubscribed from ${name}`); }
},
});console.log(greetings.get('Adam')); // will print "Hello Adam"
// Set greetings for "Adam" to "Hi Adam"
// Notice that the value comes first, and key comes after it
greetings.set('Hi Adam', 'Adam');
```### activeQuery
This is heavily based on React Query, so if you are familiar with this library you will feel like home.
```typescript
// Create a query with a factor function returning promise.
// Example: https://stackblitz.com/edit/vitejs-vite-mosens?file=src%2Fstore.ts
// Important: The query re-fetches based only on the provided parameters.
// If you use e.g. activeState in the factory function, updating it's state
// won't trigger a re-fetch.
const query = activeQuery(factory: (...args: P) => Promise, options?: {
// Number of retries in case of failure
retry?: number | false;
// onSubscribe is called when first subscriber subscribes to the state.
// The callback returned from onSubscribe is called when last subscriber unsubscribes
onSubscribe?: (...args: P) => () => void;
// Number of miliseconds the data will be cached after
// last subscriber unsubscribes (defaults to infinity)
gcTime?: number;
});// If the query for "hello" "world" has already resolved, returns the value.
// If it rejected it throws the rejection reason as an exception
// If query is pending, it throws a React Suspense error that
// suspenses rendering React components and activeComputed
query.get("hello", "world");// Returns the current state of the query. The returned state
// has the following fields that are very similar to react-query
// - status: "pending" | "success" | "error";
// - isPending: boolean;
// - isSuccess: boolean;
// - isError: boolean;
// - isRefetching: boolean;
// - isFetching: boolean;
// - isStale: boolean;
// - data?: R;
// - error?: any;
// - dataUpdatedAt?: number;
// - errorUpdatedAt?: number;
// - fetchStatus: "fetching" | "paused" | "idle";
query.state("hello", "world");// Returns a promise for query for given parameters
query.getAsync("hello" ,"world");// Invalidate query for given parameters - will mark data as stale
// and refetch if any component uses the query (either directly, or
// through activeComputed)
query.invalidateOne("hello", "world");// Invalidate query for all entries for which selector returns true.
// This marks data as stale and refetch queries used in any visible
// React component (either directly or through activeComputed)
// Options:
// - reset (default false) - reset the query to the initial state
// (idle, with no data or error)
query.invalidate(
selector: (...args: P) => boolean,
options?: { reset?: boolean }
);
```### activeComputed
Creates a computed state based on any other active state. If any React component is subscribed to it, it
recomputes automatically whenever any of its dependency changes.```typescript
// The provided factory function must not be async
// (or return a Promise) - TypeScript will complain when this happens.
// You can use any combination of activeState, activeQuery, or
// activeComputed inside.
const computed = activeComputed(factory: (...args: P) => R, options?: {
// Number of miliseconds the data will be cached after
// last subscriber unsubscribes (defaults to infinity)
gcTime?: number;
});// As active computed can depend on active query, it has to
// follow its async semantics:
// - If the computed for "hello" "world" has already resolved,
// returns the value.
// - If it rejected it throws the rejection reason
// as an exception
// - If it's pending, it throws a React Suspense error that
// suspends rendering React components and activeComputed
// that depend on it (more on this below).
computed.get("hello", "world");// Returns a promise for computed for given parameters
computed.getAsync("hello", "world");// Returns the current state of the computed value.
// The returned state has the following fields:
// - status: "pending" | "success" | "error";
// - data?
// - error?
computed.state("hello", "world");
```### useActive
It's like `useSelector` from Redux. It connects your components with store.
```typescript
// Subscribes to the value of the activeComputed property.
// Every time the value changes, component is re-rendered.
const value = useActive(() => store.computed.get(userId));// If `get` doesn't take any parameters, you can just pass
// a reference to the getter:
const value = useActive(store.currentUser.get);// For convenience you can skip the `.get` part and
// `useActive` will call it automatically:
const value = useActive(store.currentUser);
```### getActive
Compute value of an expression. If any active query or active computed is pending,
it will wait until it fully resolves```typescript
const query = activeQuery(
() => new Promise((res) => setTimeout(() => res("Hello"), 1000))
);// Will await 1s until query resolves.
// Returned value will be equal "Hello World"
const value = await getActive(() => `${query.get()} World` );
```### ActiveBoundary
Active boundary wraps a section of the app that loads and fails together.
It's basically `` and React error boundary in a single component.- fallback is used while data required to load child components is loading.
- errorFallback is used when any of the data fails to load with an exceptionYou can find an example of using it in the "Quick start" section.
## How does suspending activeQuery and activeComputed work
This library uses the [React Suspense API](https://react.dev/reference/react/Suspense) for handling loading state.
Under the hood it (ab)uses exceptions.When `activeComputed` tries to get value of an active query (e.g. `user.get('mateusz')`) that is
currently in a pending state, an special kind of exception is thrown. The trick is that the
exception is _also_ a Promise.When the promise resolves, React re-renders the component so `activeComputed` recomputes the value and this time
the query is already resolved so it successfully computes the value.## License
The project is licensed under the MIT permissive license