forked from pmndrs/jotai
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
400 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
This doc describes about the behavior with async. | ||
|
||
## Some notes | ||
|
||
- You need to wrap components with `<Suspense>` inside `<Provider>`. | ||
- You can have as many `<Suspense>` as you need. | ||
- If the `read` function of an atom returns a promise, the atom will suspend. | ||
- This applies to dependent atoms too. | ||
- If a primitive atom has a promise as the initial value, it will suspend at the first use (when Provider doesn't have it.) | ||
- You can create a `read` function so that it works asynchronouly, but does not return a promise. In such a case, the atom won't suspend. | ||
- If the `write` function of an atom returns a promise, the atom will suspend. There's no way to know as of now if an atom suspends because of `read` or `write`. | ||
- You can create a `write` function so that it works asynchronouly, but does not return a promise. In such a case, the atom won't suspend. (Caveat: This means you can't catch async errors. So, it's not recommended.) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
This doc describes about jotai core behavior. | ||
For async behavior, refer [./async.md](async.md). | ||
|
||
# API | ||
|
||
## atom | ||
|
||
`atom` is a function to create an atom config. It's an object and the object identity is important. You can create it from everywhere. Once created, you shouldn't modify the object. (Note: There might be an advanced use case to mutate atom configs after creation. At the moment, it's not officially supported though.) | ||
|
||
```js | ||
const primitiveAtom = atom(initialValue) | ||
const derivedAtomWithRead = atom(readFunction) | ||
const derivedAtomWithReadWrite = atom(readFunction, writeFunction) | ||
const derivedAtomWithWriteOnly = atom(null, writeFunction) | ||
``` | ||
|
||
There are two kinds of atoms: a writable atom and a read-only atom | ||
Primitive atoms are always writable. Derived atoms are writable if writeFunction is specified. | ||
The writeFunction of primitive atoms is equivalent to the setState of React.useState. | ||
|
||
The signature of readFunction is `(get) => Value | Promise<Value>`, and `get` is a function that takes an atom config and returns its value stored in Provider described below. | ||
Dependency is tracked, so if `get` is used for an atom at least once, then whenever the atom value is changed, the readFunction will be reevaluated. | ||
|
||
The signature of writeFunction is `(get, set, update) => void | Promise<void>`. | ||
`get` is similar to the one described above, but it doesn't track the dependency. `set` is a function that takes an atom config and a new value, and update the atom value in Provider. `update` is an arbitrary value that we receive from the updating function returned by useAtom described below. | ||
|
||
## Provider | ||
|
||
Atom configs don't hold values. Atom values are stored in a Provider. A Provider can be used like React context provider. Usually, we place one Provider at the root of the app, however you could use multiple Providers, each storing different atom values for its component tree. | ||
|
||
```js | ||
const Root = () => ( | ||
<Provider> | ||
<App /> | ||
</Provider> | ||
) | ||
``` | ||
|
||
## useAtom | ||
|
||
The useAtom hook is to read an atom value stored in the Provider. It returns the atom value and an updating function as a tuple, just like useState. It takes an atom config created with `atom()`. Initially, there is no value stored in the Provider. At the first time the atom is used via `useAtom`, it will add an initial value in the Provider. If the atom is a derived atom, the read function is executed to compute an initial value. When an atom is no longer used, meaning the component using it is unmounted, the value is removed from the Provider. | ||
|
||
```js | ||
const [value, updateValue] = useAtom(anAtom) | ||
``` | ||
|
||
The `updateValue` takes just one argument, which will be passed to the third argument of writeFunction of the atom. The behavior is totally depends on how the writeFunction is implemented. | ||
|
||
## useBridge/Bridge | ||
|
||
This will allow using accross multiple roots. | ||
You get a bridge value with `useBridge` in the outer component | ||
and pass it to `Bridge` in the inner component. | ||
|
||
```jsx | ||
const Component = ({ children }) => { | ||
const brigeValue = useBridge() | ||
return ( | ||
<AnotherRerender> | ||
<Bridge value={bridgeValue}> | ||
{children} | ||
</Bridge> | ||
</AnotherRerender> | ||
) | ||
} | ||
``` | ||
|
||
A working example: https://codesandbox.io/s/jotai-r3f-fri9d | ||
|
||
# How atom dependency works | ||
|
||
To begin with, let's explain this. In the current implementation, every time we invoke the "read" function, we refresh dependents. | ||
|
||
```js | ||
const uppercaseAtom = atom(get => get(textAtom).toUpperCase()) | ||
``` | ||
|
||
The read function is the first parameter of the atom. | ||
Initially dependency is empty. At the first use, we run the read function, and know uppercaseAtom depends on textAtom. textAtom is the dependency of uppercaseAtom. So, add uppercaseAtom to the dependents of textAtom. | ||
Next time, when we re-run the read function (because its dependency (=textAtom) is updated), | ||
we build the dependency again, which is the same in this case. we then remove stale dependents and replace with the latest one. | ||
|
||
# Some more notes about atoms | ||
|
||
- If you create a primitive atom, it will use predefined read/write functions to emulate `useState` behavior. | ||
- If you create an atom with read/write functions, they can provide any behavior with some restrictions as follows. | ||
- `read` function will be invoked during React render phase, so the function has to be pure. What is pure in React is described [here](https://gist.github.com/sebmarkbage/75f0838967cd003cd7f9ab938eb1958f). | ||
- `write` function will be invoked where you called initially and in useEffect for following invocations. So, you shouldn't call `write` in render. | ||
- When an atom is initially used with `useAtom`, it will invoke `read` function to get the initial value, this is recursive process. If an atom value exists in Provider, it will be used instead of invoking `read` function. | ||
- Once an atom is used (and stored in Provider), it's value is only updated if its dependencies are updated (including updating directly with useAtom). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# How to use jotai with next.js | ||
|
||
## Notes | ||
|
||
### When you use 'jotai/utils', you need to use the csj bundle | ||
|
||
```js | ||
import { useUpdateAtom } from 'jotai/utils.cjs' | ||
``` | ||
|
||
### You can't return promises in server side rendering | ||
|
||
```js | ||
const postData = atom((get) => { | ||
const id = get(postId) | ||
if (isSSR || prefetchedPostData[id]) { | ||
return prefetchedPostData[id] || EMPTY_POST_DATA; | ||
} | ||
return fetchData(id) // returns a promise | ||
}) | ||
``` | ||
|
||
### Hydration is possible with care | ||
|
||
Check the following examples. | ||
|
||
## Examples | ||
|
||
## Clock | ||
|
||
https://codesandbox.io/s/nextjs-with-jotai-5ylrj | ||
|
||
## HN Posts | ||
|
||
https://codesandbox.io/s/nextjs-with-jotai-async-pm0l8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
# How to persist atoms | ||
|
||
The core itself doesn't support persistence. | ||
There are some patterns for persistence depending on requirements. | ||
|
||
## A simple pattern with localStorage | ||
|
||
```js | ||
const strAtom = atom(localStorage.getItem('myKey') ?? 'foo') | ||
|
||
const strAtomWithPersistence = atom( | ||
get => get(strAtom), | ||
(get, set, newStr) => { | ||
set(strAtom, newStr) | ||
localStorage.setItem('myKey', newStr) | ||
} | ||
) | ||
``` | ||
## Combined into a single atom | ||
```js | ||
const langAtom = atom( | ||
localStorage.getItem('lang') || 'es', | ||
(get, set, newLang) => { | ||
localStorage.setItem('lang', newLang); | ||
set(langAtom, newLang) | ||
} | ||
) | ||
``` | ||
However, the above is not typescript friendly. | ||
We could use a util for typescript. | ||
```ts | ||
const langAtom = atomWithReducer( | ||
localStorage.getItem('lang') || 'es', | ||
(_prev, newLang: string) => { | ||
localStorage.setItem('lang', newLang) | ||
return newLang | ||
} | ||
) | ||
``` | ||
## A useEffect pattern | ||
```js | ||
const strAtom = atom('foo') | ||
|
||
const Component = () => { | ||
const [str, setStr] = useAtom(strAtom) | ||
useEffect(() => { | ||
if (forTheFirstTimeOrWeWantToRefresh) { | ||
const savedStr = localStorage.getItem('myKey') | ||
if (savedStr !== null) { | ||
setStr(savedStr) | ||
} | ||
} | ||
if (weWantToSaveItForThisTime) { | ||
localStorage.setItem('myKey', str) | ||
} | ||
}) | ||
) | ||
``` | ||
## A write-only atom pattern | ||
```js | ||
const createPersistAtom = (anAtom, key, serialize, deserialize) => atom( | ||
null, | ||
async (get, set, action) => { | ||
if (action.type === 'init') { | ||
const str = await AsyncStorage.getItem(key) | ||
set(anAtom, deserialize(str) | ||
} else if (action.type === 'set') { | ||
const str = serialize(get(anAtom)) | ||
await AsyncStorage.setItem(key, str) | ||
} | ||
} | ||
) | ||
``` | ||
## A serialize atom pattern | ||
```tsx | ||
const serializeAtom = atom< | ||
null, | ||
| { type: "serialize"; callback: (value: string) => void } | ||
| { type: "deserialize"; value: string } | ||
>(null, (get, set, action) => { | ||
if (action.type === "serialize") { | ||
const obj = { | ||
todos: get(todosAtom).map(get) | ||
}; | ||
action.callback(JSON.stringify(obj)); | ||
} else if (action.type === "deserialize") { | ||
const obj = JSON.parse(action.value); | ||
// needs error handling and type checking | ||
set( | ||
todosAtom, | ||
obj.todos.map((todo: Todo) => atom(todo)) | ||
); | ||
} | ||
}); | ||
|
||
const Persist: React.FC = () => { | ||
const [, dispatch] = useAtom(serializeAtom); | ||
const save = () => { | ||
dispatch({ | ||
type: "serialize", | ||
callback: (value) => { | ||
localStorage.setItem("serializedTodos", value); | ||
} | ||
}); | ||
}; | ||
const load = () => { | ||
const value = localStorage.getItem("serializedTodos"); | ||
if (value) { | ||
dispatch({ type: "deserialize", value }); | ||
} | ||
}; | ||
return ( | ||
<div> | ||
<button onClick={save}>Save to localStorage</button> | ||
<button onClick={load}>Load from localStorage</button> | ||
</div> | ||
); | ||
}; | ||
``` | ||
### Examples | ||
https://codesandbox.io/s/jotai-todos-ijyxm | ||
## A pattern with atomFamily | ||
```tsx | ||
const serializeAtom = atom< | ||
null, | ||
| { type: "serialize"; callback: (value: string) => void } | ||
| { type: "deserialize"; value: string } | ||
>(null, (get, set, action) => { | ||
if (action.type === "serialize") { | ||
const todos = get(todosAtom); | ||
const todoMap: Record<string, { title: string; completed: boolean }> = {}; | ||
todos.forEach((id) => { | ||
todoMap[id] = get(todoAtomFamily({ id })); | ||
}); | ||
const obj = { | ||
todos, | ||
todoMap, | ||
filter: get(filterAtom) | ||
}; | ||
action.callback(JSON.stringify(obj)); | ||
} else if (action.type === "deserialize") { | ||
const obj = JSON.parse(action.value); | ||
// needs error handling and type checking | ||
set(filterAtom, obj.filter); | ||
obj.todos.forEach((id: string) => { | ||
const todo = obj.todoMap[id]; | ||
set(todoAtomFamily({ id, ...todo }), todo); | ||
}); | ||
set(todosAtom, obj.todos); | ||
} | ||
}); | ||
|
||
const Persist: React.FC = () => { | ||
const [, dispatch] = useAtom(serializeAtom); | ||
const save = () => { | ||
dispatch({ | ||
type: "serialize", | ||
callback: (value) => { | ||
localStorage.setItem("serializedTodos", value); | ||
} | ||
}); | ||
}; | ||
const load = () => { | ||
const value = localStorage.getItem("serializedTodos"); | ||
if (value) { | ||
dispatch({ type: "deserialize", value }); | ||
} | ||
}; | ||
return ( | ||
<div> | ||
<button onClick={save}>Save to localStorage</button> | ||
<button onClick={load}>Load from localStorage</button> | ||
</div> | ||
); | ||
}; | ||
``` | ||
### Examples | ||
https://codesandbox.io/s/react-typescript-forked-eilkg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Showcase | ||
|
||
## Official examples | ||
|
||
### Minimal example | ||
|
||
https://codesandbox.io/s/jotai-demo-47wvh | ||
|
||
### Hacker News example | ||
|
||
https://codesandbox.io/s/jotai-demo-forked-x2g5d | ||
|
||
### Todos example | ||
|
||
https://codesandbox.io/s/jotai-demo-forked-e4cm4 | ||
|
||
### Todos example with atomFamily and localStorage | ||
|
||
https://codesandbox.io/s/react-typescript-forked-eilkg | ||
|
||
### Clock with Next.js | ||
|
||
https://codesandbox.io/s/nextjs-with-jotai-5ylrj |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# How to use jotai with typescript | ||
|
||
## Notes | ||
|
||
### Primitive atoms are basically type inferred | ||
|
||
```ts | ||
const numAtom = atom(0) // primitive number atom | ||
const strAtom = atom('') // primitive string atom | ||
``` | ||
|
||
### Primitive atoms can be explicitly typed | ||
|
||
```ts | ||
const numAtom = atom<number>(0) | ||
const numAtom = atom<number | null>(0) | ||
const arrAtom = atom<string[]>([]) | ||
``` | ||
|
||
### Derived atoms are also type inferred and explicitly typed | ||
|
||
```ts | ||
const asyncStrAtom = atom(async () => "foo") | ||
const writeOnlyAtom = atom(null, (_get, set, str: string) => set(fooAtom, str) | ||
const readWriteAtom = atom<string, number>( | ||
get => get(strAtom), | ||
(_get, set, num) => set(strAtom, String(num)) | ||
) | ||
``` | ||
### useAtom is typed based on atom types | ||
```ts | ||
const [num, setNum] = useAtom(primitiveNumAtom) | ||
const [num] = useAtom(readOnlyNumAtom) | ||
const [, setNum] = useAtom(writeOnlyNumAtom) | ||
``` |
Oops, something went wrong.