From 2ac4d3f9d7f3444cad6f846e797441987fea7bc4 Mon Sep 17 00:00:00 2001 From: zbeyens Date: Wed, 5 Feb 2025 01:09:37 +0100 Subject: [PATCH] docs --- packages/jotai-x/README.md | 581 ++++++++++++++++--------------------- 1 file changed, 245 insertions(+), 336 deletions(-) diff --git a/packages/jotai-x/README.md b/packages/jotai-x/README.md index 004eee5..c34edc5 100644 --- a/packages/jotai-x/README.md +++ b/packages/jotai-x/README.md @@ -1,423 +1,332 @@ # JotaiX -[Migrating from v1 to v2](#migrate-from-v1-to-v2) +An extension for [Jotai](https://github.com/pmndrs/jotai) that auto-generates type-safe hooks and utilities for your state. Built with TypeScript and React in mind. -JotaiX is a custom extension of [Jotai](https://github.com/pmndrs/jotai), a primitive and flexible state management library for React. Jotai offers a -minimalistic API to manage global, derived, or async states in React, solving common issues such as unnecessary -re-renders or complex context management. JotaiX builds upon this foundation, providing enhanced utilities and patterns -for more efficient and streamlined state management in larger and more complex applications. +## Features -`jotai-x`, built on top of `jotai`, is providing a powerful store factory -which solves these challenges, so you can focus on your app. +- Auto-generated type-safe hooks for each state field +- Simple patterns: `useStoreValue('name')` and `useStoreSet('name', value)` +- Extend your store with computed values using `extend` +- Built-in support for hydration, synchronization, and scoped providers -```bash -yarn add jotai jotai-x -``` +## Why -For further details and API documentation, visit [jotai-x.udecode.dev](https://jotai-x.udecode.dev). +Built on top of `jotai`, `jotai-x` offers a better developer experience with less boilerplate. Create and interact with stores faster using a more intuitive API. -## **Why Choose `jotai-x`?** +> Looking for global state management instead of React Context-based state? Check out [Zustand X](https://github.com/udecode/zustand-x) - same API, different state model. -- Reduces boilerplate: Simplifies state management with concise and powerful utilities. -- Enhanced modular state management: Offers advanced features like atom stores, hydration utilities, and more. -- Improved developer experience: Strong TypeScript support ensures type safety and better developer tooling. -- Seamless integration with Jotai: Builds on top of Jotai's API, making it easy for existing Jotai users to adopt. +## Installation -## **Core Features** +```bash +pnpm add jotai jotai-x +``` -### **Creating a Store** +## Quick Start -JotaiX allows for the creation of structured stores with ease, integrating seamlessly with Jotai's atom concept. +Here's how to create a simple store: ```tsx import { createAtomStore } from 'jotai-x'; -// Notice how it uses the name of the store in the returned object. -export const { useElementStore, ElementProvider } = createAtomStore({ - element: null -}, { - name: 'element' -}); -``` +// Create a store with an initial state +// Store name is used as prefix for all returned hooks (e.g., `useAppStore`, `useAppValue` for `name: 'app'`) +const { useAppStore, useAppValue, useAppSet, useAppState, AppProvider } = + createAtomStore( + { + name: 'JotaiX', + stars: 0, + }, + { + name: 'app', + } + ); -The **`createAtomStore`** function simplifies the process of creating and managing atom-based states. +// Use it in your components +function RepoInfo() { + const name = useAppValue('name'); + const stars = useAppValue('stars'); -#### Function Signature + return ( +
+

{name}

+

{stars} stars

+
+ ); +} -```tsx -createAtomStore(initialState: T, options?: CreateAtomStoreOptions): AtomStoreApi; +function AddStarButton() { + const setStars = useAppSet('stars'); + + return ; +} ``` -- **`initialState`**: This is an object representing the initial state of your store. Each key-value pair in this object is used to create an individual atom. This is required even if you want to set the initial value from the provider, otherwise the atom would not be created. -- **`options`**: Optional. This parameter allows you to pass additional configuration options for the store creation. - -#### Options - -The **`options`** object can include several properties to customize the behavior of your store: - -- **`name`**: A string representing the name of the store, which can be helpful for debugging or when working with multiple stores. -- **`delay`**: If you need to introduce a delay in state updates, you can specify it here. Optional. -- **`effect`**: A React component that can be used to run effects inside the provider. Optional. -- **`extend`**: Extend the store with derived atoms based on the store state. Optional. -- **`infiniteRenderDetectionLimit`**: In non production mode, it will throw an error if the number of `useValue` hook calls exceeds this limit during the same render. Optional. - -#### Return Value - -The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) containing the following properties and methods for interacting with the store: - -- **`useStore`**: - - A function that returns the following objects: **`useValue`**, **`useSet`**, **`useState`**, where values are hooks for each state defined in the store, and **`get`**, **`set`**, **`subscribe`**, **`store`**, where values are direct get/set accessors to modify each state. - - **`useValue`**: Hooks for accessing a state within a component, ensuring re-rendering when the state changes. See [useAtomValue](https://jotai.org/docs/core/use-atom#useatomvalue). - ``` js - const store = useElementStore(); - - const element = useStoreValue('element'); - // alternative - const element = store.useElementValue(); - // alternative - const element = useElementStore().useValue('element'); - ``` - - Advanced: `useValue` supports parameters `selector`, which is a function that takes the current value and returns a new value and parameter `equalityFn`, which is a function that compares the previous and new values and only re-renders if they are not equal. Internally, it uses [selectAtom](https://jotai.org/docs/utilities/select#selectatom). You must memoize `selector`/`equalityFn` adequately. - ``` js - const store = useElementStore(); - - // Approach 1: Memoize the selector yourself - const toUpperCase = useCallback((element) => element.toUpperCase(), []); - // Now it will only re-render if the uppercase value changes - const element = useStoreValue(store, 'element', toUpperCase); - // alternative - const element = store.useElementValue(toUpperCase); - // alternative - const element = useElementStore().useValue('element', toUpperCase); - - // Approach 2: Pass an dependency array to prevent re-renders - const [n, setN] = useState(0); // n may change during re-renders - const numNthCharacter = useStoreValue(store, 'element', (element) => element[n], [n]); - // alternative - const numNthCharacter = store.useElementValue((element) => element[n], [n]); - // alternative - const numNthCharacter = store.useValue('element', (element) => element[n], [n]); - ``` - - **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom). - ``` js - const store = useElementStore(); - const element = useStoreSet(store, 'element'); - // alternative - const element = store.useSetElement(); - // alternative - const element = useElementStore().useSet('element'); - ``` - - **`useState`**: Hooks for accessing and setting a state within a component, ensuring re-rendering when the state changes. See [useAtom](https://jotai.org/docs/core/use-atom). - ``` js - const store = useElementStore(); - const element = useStoreState(store, 'element'); - // alternative - const element = store.useElementState(); - // alternative - const element = useElementStore().useState('element'); - ``` - - **`get`**: Directly get the state. Not a hook so it could be used in event handlers or other hooks, and the component won't re-render if the state changes. See [createStore](https://jotai.org/docs/core/store#createstore) - ``` js - const store = useElementStore(); - useEffect(() => { console.log(store.getElement()) }, []); - // alternative - useEffect(() => { console.log(store.get('element')) }, []); - ``` - - **`set`**: Directly set the state. Not a hook so it could be used in event handlers or other hooks. See [createStore](https://jotai.org/docs/core/store#createstore) - ``` js - const store = useElementStore(); - useEffect(() => { store.setElement('div') }, []); - // alternative - useEffect(() => { store.set('element', 'div') }, []); - ``` - - **`subscribe`**: Subscribe to the state change. . See [createStore](https://jotai.org/docs/core/store#createstore) - - NOTE: The subscribed callback will fire whenever the atom state or dependent atom states change. There is no equality check. - ``` js - const store = useElementStore(); - useEffect(() => store.subscribeElement((newElement) => console.log(newElement)), []); - // alternative - useEffect(() => store.subscribe('element', (newElement) => console.log(newElement)), []); - ``` - - **`store`**: The [JotaiStore](https://jotai.org/docs/core/store) for the current context. - ``` js - const store = useElementStore(); - const jotaiStore = store.store; - ``` -- **`Provider`**: - - The API includes dynamically generated provider components for each defined store. This allows scoped state management within your application. More information in the next section. -- **`Store`**: - - **`atom`**: Access the atoms used by the store, including derived atoms defined using `extend`. See [atom](https://jotai.org/docs/core/atom). - -### **Provider-Based Store Hydration and Synchronization** - -**`createAtomStore`** generates a provider component (`Provider`) for a Jotai store. This provider not only supplies the store to its child components but also handles hydrating and syncing the store's state. Here's how it works: - -- **Hydration**: Hydrates atoms with initial values. It's particularly useful for SSR, ensuring that the client-side state aligns with what was rendered on the server. Use `initialValues` prop. -- **Synchronization**: Updates atoms with new values as external changes occur, maintaining consistency across the application. Use `` props: there is one for each state defined in the store. - -### Scoped Providers and Context Management - -JotaiX creates scoped providers, enabling more granular control over different segments of state within your application. `createAtomStore` sets up a context for each store, which can be scoped using the **`scope`** prop. This is particularly beneficial in complex applications where nested providers are needed. +## Core Concepts -### Derived Atoms +### Store Configuration + +The store is where everything begins. Configure it with type-safe options: -There are two ways of creating derived atoms from your JotaiX store. +```ts +import { createAtomStore } from 'jotai-x'; -#### Derived Atoms Using `extend` +// Types are inferred, including options +const { useUserValue, useUserSet, useUserState, UserProvider } = + createAtomStore( + { + name: 'Alice', + loggedIn: false, + }, + { + name: 'user', + delay: 100, // Optional delay for state updates + effect: EffectComponent, // Optional effect component + extend: (atoms) => ({ + // Optional derived atoms + intro: atom((get) => `My name is ${get(atoms.name)}`), + }), + infiniteRenderDetectionLimit: 100, // Optional render detection limit + } + ); +``` -Atoms defined using the `extend` option are made available in the same places as other values in the store. +Available options: ```ts -const { useUserStore } = createAtomStore({ - username: 'Alice', -}, { - name: 'user', - extend: (atoms) => ({ - intro: atom((get) => `My name is ${get(atoms.username)}`), - }), -}); - -const userStore = useUserStore(); -const intro = userStore.useIntroValue(); +{ + name: string; + delay?: number; + effect?: React.ComponentType; + extend?: (atoms: Atoms) => DerivedAtoms; + infiniteRenderDetectionLimit?: number; +} ``` -#### Externally Defined Derived Atoms +### Reading and Writing State + +The API is designed to be intuitive. Here's how you work with state: -Derived atoms can also be defined externally by accessing the store's atoms through the `Store` API. Externally defined atoms can be accessed through the store using special hooks: -`useAtomValue`, `useSetAtom`, `useAtomState`, `getAtom`, `setAtom`, `subscribeAtom`. +#### Using Hooks (Recommended) ```ts -const { userStore, useUserStore } = createAtomStore({ - username: 'Alice', -}, { name: 'user' }); +// Get a single value +const name = useUserValue('name'); +const loggedIn = useUserValue('loggedIn'); -const introAtom = atom((get) => `My name is ${get(userStore.atom.username)}`); +// Get a setter +const setName = useUserSet('name'); +const setLoggedIn = useUserSet('loggedIn'); -const userStore = useUserStore(); -const intro = userStore.useAtomValue(introAtom); +// Get both value and setter +const [name, setName] = useUserState('name'); +const [loggedIn, setLoggedIn] = useUserState('loggedIn'); + +// With selector +const upperName = useUserValue('name', (name) => name.toUpperCase()); + +// With selector and deps +const nthChar = useUserValue('name', (name) => name[n], [n]); ``` -### Example Usage +### React Hooks -#### 1. Create a store +#### `useStoreValue(key, selector?, deps?)` -```tsx -import { createAtomStore } from 'jotai-x'; +Subscribe to a single value. Optionally pass a selector and deps array: -export type AppStore = { - name: string; - onUpdateName: (name: string) => void; -}; +```ts +// Basic usage +const name = useUserValue('name'); -const initialState: Nullable = { - name: null, - onUpdateName: null, -}; +// With selector +const upperName = useUserValue('name', (name) => name.toUpperCase()); -export const { useAppStore, AppProvider } = createAtomStore( - initialState as AppStore, - { name: 'app' } -); +// With selector and deps +const nthChar = useUserValue('name', (name) => name[n], [n]); +``` + +#### `useStoreSet(key)` + +Get a setter function for a value: + +```ts +// Basic usage +const setName = useUserSet('name'); ``` -#### 2. Use the store in a component +#### `useStoreState(key)` + +Get a value and its setter, just like React's `useState`: ```tsx -// ... +function UserForm() { + const [name, setName] = useUserState('name'); + const [email, setEmail] = useUserState('email'); -const App = () => { return ( - console.log(name) - }} - // Either here or in initialValues - name="John Doe" - > - - - ); -}; - -const Component = () => { - const appStore = useAppStore(); - const [name, setName] = store.useNameState(); - const onUpdateName = store.useOnUpdateNameValue(); - - useEffect(() => store.subscribe.name((newName) => { - console.log(`Name updated to: ${newName}`); - // An alternative to `appStore.useNameState()`, won't rerender when the state changes - assert.ok(newName === appStore.getName()); - if (newName.includes('#')) { - // Equivalent to `appStore.useSetName()`'s return - appStore.setName('invalid'); - onUpdateName('invalid'); - } - }), [appStore]) - - return ( -
+
setName(e.target.value)} /> - -
+ setEmail(e.target.value)} /> + ); -}; +} ``` -#### Scoped Providers +### Provider-Based Store Hydration + +The provider component handles hydrating and syncing the store's state: ```tsx -const App = () => { +function App() { return ( - // Parent scope - console.log("Parent:", name) + name: 'Alice', + onUpdateName: (name) => console.log(name), }} - name="Parent User" + // Alternative to initialValues + name="Alice" > -
-

Parent Component

- - {/* Child scope */} - console.log("Child:", name) - }} - name="Child User" - > -
-

Child Component

- -
-
-
-
- ); -}; - -// Accessing state from the specified scope. -const Component = () => { - // Here, we get the state from the parent scope - const parentAppStore = useAppStore('parent'); - const [name, setName] = parentScope.useNameState(); - // Here, we get the state from the closest scope (default) - const appStore = useAppStore(); - const onUpdateName = appStore.useOnUpdateNameValue(); - - return ( -
- setName(e.target.value)} /> - -
+ + ); -}; +} ``` -## **Troubleshooting** -### Error: useValue/useValue has rendered `num` times in the same render -When calling `useValue` or `useValue` with `selector` and `equalityFn`, those two functions must be memoized. Otherwise, the component will re-render infinitely. In order to prevent developers from making this mistake, in non-production mode (`process.env.NODE_ENV !== 'production'`), we will throw an error if the number of `useValue` hook calls exceeds a certain limit. +### Scoped Providers -Usually, this error is caused by not memoizing the `selector` or `equalityFn` functions. To fix this, you can use `useCallback` to memoize the functions, or pass a dependency array yourselves. We support multiple alternatives: +Create multiple instances of the same store with different scopes: ```tsx -// No selector at all -useValue('key') +function App() { + return ( + + + + + + ); +} + +function UserProfile() { + // Get parent scope + const parentName = useUserValue('name', { scope: 'parent' }); + // Get closest scope + const name = useUserValue('name'); +} +``` -// Memoize with useCallback yourself -const memoizedSelector = useCallback(selector, [...]); -const memoizedEqualityFn = useCallback(equalityFn, [...]); -// memoizedEqualityFn is an optional parameter -useValue('key', memoizedSelector, memoizedEqualityFn); +### Derived Atoms -// Provide selector and its deps -useValue('key', selector, [...]); +Two ways to create derived atoms: -// Provide selector and equalityFn and all of their deps -useValue('key', selector, equalityFn, [...]); -``` +```ts +// 1. Using extend +const { useUserValue } = createAtomStore( + { + name: 'Alice', + }, + { + name: 'user', + extend: (atoms) => ({ + intro: atom((get) => `My name is ${get(atoms.name)}`), + }), + } +); -The error could also be a false positive, since the internal counter is shared across all `useValue` calls of the same store. If your component tree is very deep and uses the same store's `useValue` multiple times, then the limit could be reached. To deal with that, `createAtomStore` supports an optional parameter `infiniteRenderDetectionLimit`. You can configure that with a higher limit. +// Access the derived value using the store name +const intro = useUserValue('intro'); + +// 2. External atoms +const { userStore, useUserStore } = createAtomStore( + { + name: 'Alice', + }, + { + name: 'user', + } +); -## **Migrate from v1 to v2** +// Create an external atom +const introAtom = atom((get) => `My name is ${get(userStore.atom.name)}`); -1. Return of `useStore`: `get` is renamed to `useValue`, `set` is renamed to `useSet`, `use` is renamed to `useState`. -``` diff -- const name = useAppStore().get.name(); -- const setName = useAppStore().set.name(); -- const [name, setName] = useAppStore().use.name(); +// Create a writable external atom +const countAtom = atom( + (get) => get(userStore.atom.name).length, + (get, set, newCount: number) => { + set(userStore.atom.name, 'A'.repeat(newCount)); + } +); -+ const appStore = useAppStore(); -+ const name = appStore.useNameValue(); -+ const setName = appStore.useSetName(); -+ const [name, setName] = appStore.useNameState(); +// Get the store instance +const store = useUserStore(); -+ // alternative -+ const name = appStore.useValue('name'); -+ const setName = appStore.useSet('name'); -+ const [name, setName] = appStore.useState('name'); -``` +// Access external atoms using store-based atom hooks +const intro = useAtomValue(store, introAtom); // Read-only atom +const [count, setCount] = useAtomState(store, countAtom); // Read-write atom +const setCount2 = useSetAtom(store, countAtom); // Write-only -2. Rename `.atom()` APIs: -``` diff -- const atomValue = useAppStore().get.atom(atomConfig); -- const setAtomValue = useAppStore().set.atom(atomConfig); -- const [atomValue, setAtomValue] = useAppStore().use.atom(atomConfig); +// With selector and deps +const upperIntro = useAtomValue( + store, + introAtom, + (intro) => intro.toUpperCase(), + [] // Optional deps array for selector +); -+ const appStore = useAppStore(); -+ const atomValue = appStore.useAtomValue(atomConfig); -+ const setAtomValue = appStore.useSetAtom(atomConfig); -+ const [atomValue, setAtomValue] = appStore.useAtomState(atomConfig); +// With selector and equality function +const intro2 = useAtomValue( + store, + introAtom, + (intro) => intro, + (prev, next) => prev.length === next.length // Optional equality function +); ``` -NOTE: Try to avoid using the key "atom" as the store state key because - 1. `useValue('atom')` and `useSet('atom')` and `useState('atom')` are not supported. They are only valid if the key "atom" is presented in the store. - 2. On the other hand, `useAtomValue()`, `useSetAtom()`, and `useAtomState()` cannot access the state if the key "atom" is presented in the store. -3. Return of `useStore`: `store` is no longer a function. Now it is a direct property. -``` diff -- const store = useAppStore().store(); +The store-based atom hooks provide more flexibility when working with external atoms: -+ const appStore = useAppStore(); -+ const jotaiStore = appStore.store; -``` +- `useAtomValue(store, atom, selector?, equalityFnOrDeps?, deps?)`: Subscribe to a read-only atom value + - `selector`: Transform the atom value (must be memoized or use deps) + - `equalityFnOrDeps`: Custom comparison function or deps array + - `deps`: Dependencies array when using both selector and equalityFn +- `useSetAtom(store, atom)`: Get a setter function for a writable atom +- `useAtomState(store, atom)`: Get both value and setter for a writable atom, like React's `useState` -4. Return of `useStore`: `option` is no longer a valid parameter of `useValue` and `useSet`. To control the behavior, directly pass the options to `createAtomStore` or `useStore`. -``` diff -- const scope1Name = useAppStore().useValue.name(scope1Options); -- const scope2Name = useAppStore().useValue.name(scope2Options); +## Troubleshooting -+ const scope1AppStore = useAppStore(scope1Options); -+ const scope1Name = scope1AppStore.useNameValue(); -+ const scope2AppStore = useAppStore(scope2Options); -+ const scope2Name = scope2AppStore.useNameValue(); -``` +### Infinite Render Detection -## Contributing +When using value hooks with selectors, ensure they are memoized: -### Ideas and discussions +```tsx +// ❌ Wrong - will cause infinite renders +useUserValue('name', { selector: (name) => name.toUpperCase() }); -[Discussions](https://github.com/udecode/jotai-x/discussions) is the best -place for bringing opinions and contributions. Letting us know if we're -going in the right or wrong direction is great feedback and will be much -appreciated! +// ✅ Correct - memoize with useCallback +const selector = useCallback((name) => name.toUpperCase(), []); +useUserValue('name', { selector }); -#### [Become a Sponsor!](https://github.com/sponsors/zbeyens) +// ✅ Correct - provide deps array +useUserValue('name', { selector: (name) => name.toUpperCase(), deps: [] }); -### Contributors +// ✅ Correct - no selector +useUserValue('name'); +``` -🌟 Stars and 📥 Pull requests are welcome! Don't hesitate to **share -your feedback** here. Read our -[contributing guide](https://github.com/udecode/jotai-x/blob/main/CONTRIBUTING.md) -to get started. +## Migration from v1 to v2 -

- - Deploys by Netlify - -

+```ts +// Before +const name = useAppStore().get.name(); +const setName = useAppStore().set.name(); +const [name, setName] = useAppStore().use.name(); + +// Now +const name = useAppValue('name'); +const setName = useAppSet('name'); +const [name, setName] = useAppState('name'); +``` ## License -[MIT](https://github.com/udecode/jotai-x/blob/main/LICENSE) +[MIT](./LICENSE)