{"id":20365204,"url":"https://github.com/udecode/jotai-x","last_synced_at":"2025-04-12T04:46:31.400Z","repository":{"id":211414050,"uuid":"729065064","full_name":"udecode/jotai-x","owner":"udecode","description":"Jotai store factory for a best-in-class developer experience.","archived":false,"fork":false,"pushed_at":"2025-03-01T13:49:40.000Z","size":2431,"stargazers_count":23,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-12T04:46:21.215Z","etag":null,"topics":["jotai","react","state-management"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/udecode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-12-08T10:26:27.000Z","updated_at":"2025-03-01T13:49:06.000Z","dependencies_parsed_at":"2024-04-15T12:31:43.438Z","dependency_job_id":"58960d33-cd0c-40c5-afd4-69630f231363","html_url":"https://github.com/udecode/jotai-x","commit_stats":null,"previous_names":["udecode/jotai-x"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/udecode%2Fjotai-x","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/udecode%2Fjotai-x/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/udecode%2Fjotai-x/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/udecode%2Fjotai-x/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/udecode","download_url":"https://codeload.github.com/udecode/jotai-x/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248519471,"owners_count":21117757,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jotai","react","state-management"],"created_at":"2024-11-15T00:16:18.914Z","updated_at":"2025-04-12T04:46:31.392Z","avatar_url":"https://github.com/udecode.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Jotai X\n\nAn 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.\n\n## Features\n\n- Auto-generated type-safe hooks for each state field\n- Simple patterns: `use\u003cStoreName\u003eValue(key)` and `use\u003cStoreName\u003eSet(key, value)`\n- Extend your store with computed values using `extend`\n- Built-in support for hydration, synchronization, and scoped providers\n\n## Why\n\nBuilt 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.\n\n\u003e 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.\n\n## Installation\n\n```bash\npnpm add jotai jotai-x\n```\n\n## Quick Start\n\nHere's how to create a simple store:\n\n```tsx\nimport { createAtomStore } from 'jotai-x';\n\n// Create a store with an initial state\n// Store name is used as prefix for all returned hooks (e.g., `useAppStore`, `useAppValue` for `name: 'app'`)\nconst { useAppStore, useAppValue, useAppSet, useAppState, AppProvider } =\n  createAtomStore(\n    {\n      name: 'JotaiX',\n      stars: 0,\n    },\n    {\n      name: 'app',\n    }\n  );\n\n// Use it in your components\nfunction RepoInfo() {\n  const name = useAppValue('name');\n  const stars = useAppValue('stars');\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003e{name}\u003c/h1\u003e\n      \u003cp\u003e{stars} stars\u003c/p\u003e\n    \u003c/div\u003e\n  );\n}\n\nfunction AddStarButton() {\n  const setStars = useAppSet('stars');\n\n  return \u003cbutton onClick={() =\u003e setStars((s) =\u003e s + 1)}\u003eAdd star\u003c/button\u003e;\n}\n```\n\n## Core Concepts\n\n### Store Configuration\n\nThe store is where everything begins. Configure it with type-safe options:\n\n```ts\nimport { createAtomStore } from 'jotai-x';\n\n// Types are inferred, including options\nconst { useUserValue, useUserSet, useUserState, UserProvider } =\n  createAtomStore(\n    {\n      name: 'Alice',\n      loggedIn: false,\n    },\n    {\n      name: 'user',\n      delay: 100, // Optional delay for state updates\n      effect: EffectComponent, // Optional effect component\n      extend: (atoms) =\u003e ({\n        // Optional derived atoms\n        intro: atom((get) =\u003e `My name is ${get(atoms.name)}`),\n      }),\n      infiniteRenderDetectionLimit: 100, // Optional render detection limit\n    }\n  );\n```\n\nAvailable options:\n\n```ts\n{\n  name: string;\n  delay?: number;\n  effect?: React.ComponentType;\n  extend?: (atoms: Atoms) =\u003e DerivedAtoms;\n  infiniteRenderDetectionLimit?: number;\n}\n```\n\n### Store API\n\nThe `createAtomStore` function returns an object with the following:\n\n```ts\nconst {\n  // Store name used as prefix\n  name: string,\n\n  // Store hook returning all utilities\n  useAppStore: () =\u003e StoreApi,\n\n  // Direct hooks for state management\n  useAppValue: (key: string, options?) =\u003e Value,\n  useAppSet: (key: string) =\u003e SetterFn,\n  useAppState: (key: string) =\u003e [Value, SetterFn],\n\n  // Provider component\n  AppProvider: React.FC\u003cProviderProps\u003e,\n\n  // Record of all atoms in the store\n  appStore: {\n    atom: Record\u003cstring, Atom\u003e\n  }\n} = createAtomStore({ ... }, { name: 'app' });\n```\n\n### Reading and Writing State\n\nThere are three ways to interact with the store state:\n\n#### 1. Hooks (Recommended)\n\nThe most straightforward way using hooks returned by `createAtomStore`:\n\n```ts\n// Get value\nconst name = useAppValue('name');\nconst stars = useAppValue('stars');\n\n// Set value\nconst setName = useAppSet('name');\nconst setStars = useAppSet('stars');\n\n// Get both value and setter\nconst [name, setName] = useAppState('name');\nconst [stars, setStars] = useAppState('stars');\n\n// With selector and deps\nconst upperName = useAppValue('name', {\n  selector: (name) =\u003e name.toUpperCase(),\n}, []);\n```\n\n#### 2. Store Instance Methods\n\nUsing the store instance from `useAppStore()`:\n\n```ts\nconst store = useAppStore();\n\n// By key\nstore.get('name'); // Get value\nstore.set('name', 'value'); // Set value\nstore.subscribe('name', (value) =\u003e console.log(value)); // Subscribe to changes\n\n// Direct access\nstore.getName(); // Get value\nstore.setName('value'); // Set value\nstore.subscribeName((value) =\u003e console.log(value)); // Subscribe to changes\n```\n\n#### 3. Raw Atom Access\n\nFor advanced use cases, you can work directly with atoms:\n\n```ts\nconst store = useAppStore();\n\n// Access atoms\nstore.getAtom(someAtom); // Get atom value\nstore.setAtom(someAtom, 'value'); // Set atom value\nstore.subscribeAtom(someAtom, (value) =\u003e {}); // Subscribe to atom\n\n// Access underlying Jotai store\nconst jotaiStore = store.store;\n```\n\n### Hook API Reference\n\n#### `use\u003cName\u003eValue(key, options?)`\n\nSubscribe to a single value with optional selector and deps:\n\n```ts\n// Basic usage\nconst name = useAppValue('name');\n\n// With selector\nconst upperName = useAppValue('name', {\n  selector: (name) =\u003e name.toUpperCase(),\n}, [] // if selector is not memoized, provide deps array\n);\n\n// With equality function\nconst name = useAppValue('name', {\n  selector: (name) =\u003e name,\n  equalityFn: (prev, next) =\u003e prev.length === next.length\n}, []);\n```\n\n#### `use\u003cName\u003eSet(key)`\n\nGet a setter function for a value:\n\n```ts\nconst setName = useAppSet('name');\nsetName('new value');\nsetName((prev) =\u003e prev.toUpperCase());\n```\n\n#### `use\u003cName\u003eState(key)`\n\nGet both value and setter, like React's `useState`:\n\n```tsx\nfunction UserForm() {\n  const [name, setName] = useAppState('name');\n  const [email, setEmail] = useAppState('email');\n\n  return (\n    \u003cform\u003e\n      \u003cinput value={name} onChange={(e) =\u003e setName(e.target.value)} /\u003e\n      \u003cinput value={email} onChange={(e) =\u003e setEmail(e.target.value)} /\u003e\n    \u003c/form\u003e\n  );\n}\n```\n\n### Provider-Based Store Hydration\n\nThe provider component handles store initialization and state synchronization:\n\n```tsx\ntype ProviderProps\u003cT\u003e = {\n  // Initial values for atoms, hydrated once on mount\n  initialValues?: Partial\u003cT\u003e;\n\n  // Dynamic values for controlled state\n  ...Partial\u003cT\u003e;\n\n  // Optional custom store instance\n  store?: JotaiStore;\n\n  // Optional scope for nested providers\n  scope?: string;\n\n  // Optional key to reset the store\n  resetKey?: any;\n\n  children: React.ReactNode;\n};\n\nfunction App() {\n  return (\n    \u003cUserProvider\n      // Initial values hydrated on mount\n      initialValues={{\n        name: 'Alice',\n        email: 'alice@example.com'\n      }}\n\n      // Controlled values that sync with the store\n      name=\"Bob\"\n\n      // Optional scope for nested providers\n      scope=\"user1\"\n\n      // Optional key to reset store state\n      resetKey={version}\n    \u003e\n      \u003cUserProfile /\u003e\n    \u003c/UserProvider\u003e\n  );\n}\n```\n\n### Scoped Providers\n\nCreate multiple instances of the same store with different scopes:\n\n```tsx\nfunction App() {\n  return (\n    \u003cUserProvider scope=\"parent\" name=\"Parent User\"\u003e\n      \u003cUserProvider scope=\"child\" name=\"Child User\"\u003e\n        \u003cUserProfile /\u003e\n      \u003c/UserProvider\u003e\n    \u003c/UserProvider\u003e\n  );\n}\n\nfunction UserProfile() {\n  // Get parent scope\n  const parentName = useUserValue('name', { scope: 'parent' });\n  // Get closest scope\n  const name = useUserValue('name');\n}\n```\n\n### Derived Atoms\n\nTwo ways to create derived atoms:\n\n```ts\n// 1. Using extend\nconst { useUserValue } = createAtomStore(\n  {\n    name: 'Alice',\n  },\n  {\n    name: 'user',\n    extend: (atoms) =\u003e ({\n      intro: atom((get) =\u003e `My name is ${get(atoms.name)}`),\n    }),\n  }\n);\n\n// Access the derived value using the store name\nconst intro = useUserValue('intro');\n\n// 2. External atoms\nconst { userStore, useUserStore } = createAtomStore(\n  {\n    name: 'Alice',\n  },\n  {\n    name: 'user',\n  }\n);\n\n// Create an external atom\nconst introAtom = atom((get) =\u003e `My name is ${get(userStore.atom.name)}`);\n\n// Create a writable external atom\nconst countAtom = atom(\n  (get) =\u003e get(userStore.atom.name).length,\n  (get, set, newCount: number) =\u003e {\n    set(userStore.atom.name, 'A'.repeat(newCount));\n  }\n);\n\n// Get the store instance\nconst store = useUserStore();\n\n// Access external atoms using store-based atom hooks\nconst intro = useAtomValue(store, introAtom); // Read-only atom\nconst [count, setCount] = useAtomState(store, countAtom); // Read-write atom\nconst setCount2 = useSetAtom(store, countAtom); // Write-only\n\n// With selector and deps\nconst upperIntro = useAtomValue(\n  store,\n  introAtom,\n  (intro) =\u003e intro.toUpperCase(),\n  [] // Optional deps array for selector\n);\n\n// With selector and equality function\nconst intro2 = useAtomValue(\n  store,\n  introAtom,\n  (intro) =\u003e intro,\n  (prev, next) =\u003e prev.length === next.length // Optional equality function\n);\n```\n\nThe store-based atom hooks provide more flexibility when working with external atoms:\n\n- `useAtomValue(store, atom, selector?, equalityFnOrDeps?, deps?)`: Subscribe to a read-only atom value\n  - `selector`: Transform the atom value (must be memoized or use deps)\n  - `equalityFnOrDeps`: Custom comparison function or deps array\n  - `deps`: Dependencies array when using both selector and equalityFn\n- `useSetAtom(store, atom)`: Get a setter function for a writable atom\n- `useAtomState(store, atom)`: Get both value and setter for a writable atom, like React's `useState`\n\n## Troubleshooting\n\n### Infinite Render Detection\n\nWhen using value hooks with selectors, ensure they are memoized:\n\n```tsx\n// ❌ Wrong - will cause infinite renders\nuseUserValue('name', { selector: (name) =\u003e name.toUpperCase() });\n\n// ✅ Correct - memoize with useCallback\nconst selector = useCallback((name) =\u003e name.toUpperCase(), []);\nuseUserValue('name', { selector });\n\n// ✅ Correct - provide deps array\nuseUserValue('name', { selector: (name) =\u003e name.toUpperCase() }, []);\n\n// ✅ Correct - no selector\nuseUserValue('name');\n```\n\n## Migration from v1 to v2\n\n```ts\n// Before\nconst { useAppStore } = createAtomStore({ name: 'Alice' }, { name: 'app' });\nconst name = useAppStore().get.name();\nconst setName = useAppStore().set.name();\nconst [name, setName] = useAppStore().use.name();\n\n// Now\nconst { useAppStore, useAppValue, useAppSet, useAppState } = createAtomStore({ name: 'Alice' }, { name: 'app' });\nconst name = useAppValue('name');\nconst setName = useAppSet('name');\nconst [name, setName] = useAppState('name');\n```\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fudecode%2Fjotai-x","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fudecode%2Fjotai-x","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fudecode%2Fjotai-x/lists"}