Jotai 공식 문서에서 Core 라이브러리에 대한 내용을 조금 더 정리해 보려고 한다!!
atom()
함수는 atom config를 생성함import { atom } from 'jotai'
const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })
또한 3가지 패턴으로 derived atoms를 만들 수 있음
Read-only
atomWrite-only
atomRead-Write
atomderived atoms를 생성하기 위해서는, read 함수를 제공하고, optional하게 write 함수도 제공하면 됨
const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
null, // it's a convention to pass `null` for the first argument
(get, set, update) => {
// `update` is any single value we receive for updating this atom
set(priceAtom, get(priceAtom) - update.discount)
}
)
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2)
// you can set as many atoms as you want at the same time
}
)
get
인 함수를 넣는 게 컨벤션이고,null
을 넣고, 두 번째 인자에 파라미터가 get
, set
, update
인 함수를 넣는 게 컨벤션인듯update
변수의 이름은 달라져도 됨get
은 atom value를 읽기 위함get
은 마찬가지로 atom value를 읽기 위함이지만, 추적되지는 않음!set
은 atom value를 쓰기 위함Note about creating an atom in render function
useMemo
나 useRef
는 안정적인 참조를 위해 필요함useMemo
를 쓸지 useRef
를 쓸지 고민되면 useMemo
를 써라useRef
를 쓰면, useAtom
과 함께 쓸 때 무한 루프를 발생시킬 수도 있음const Component = ({ value }) => {
const valueAtom = useMemo(() => atom({ value }), [value])
// ...
}
Signatures
// primitive atom
function atom<Value>(initialValue: Value): PrimitiveAtom<Value>
// read-only atom
function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>
// writable derived atom
function atom<Value, Update>(
read: (get: Getter) => Value | Promise<Value>,
write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>
// write-only derived atom
function atom<Value, Update>(
read: Value,
write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>
initialValue
: atom이 값이 바뀔 때까지 리턴할 초기값
read
: 다시 리렌더링 할 때마다 호출하는 함수.
read
의 signature는 (get) => Value | Promise<Value>
이고, get
은 아래 설명된 대로, atom config를 가져와 Provider에 저장된 값을 반환하는 함수get
을 1번 이상 사용하면, atom value가 변경될 때마다 read
가 재평가됨write
: atom value를 변경하는 데 주로 사용되는 함수
useAtom
쌍의 2번째 값인 useAtom()
호출할 때마다 호출됨write
의 signature는 (get, set, upgrade) => void | Promise<void>
get
은 read
에서 설명한 get
과 비슷해 보이지만, dependency를 추적하지 않음set
은 atom config와 새로운 값을 가지고, Provider에서 atom value를 업데이트 하는 함수update
는 아래에 설명될 useAtom
이 반환한 업데이트 함수에서 받은 임의의 값✨ 대충 요약하자면 read
에 있는 get
은 값이 추적되므로, atom 값이 바뀌면 read
가 재평가되는데, write
에 있는 get
은 값을 추적하고 있지는 않기 때문에 get
을 사용해도 write
가 재평가되지는 않는듯?!
const primitiveAtom = atom(initialValue)
const derivedAtomWithRead = atom(read)
const derivedAtomWithReadWrite = atom(read, write)
const derivedAtomWithWriteOnly = atom(null, write)
write
가 명시되어 있으면 writablewrite
는 React.useState
의 setState
와 동등함debugLabel
property
debugLabel
을 가짐onMount
property
onMount
를 가짐onMount
는 setAtom
함수를 취하고, optional하게 onUnmount
함수를 리턴하는 함수onMount
함수는 provider에서 atom이 처음으로 사용될 때 호출되며, onUnmount
함수는 그것이 더 이상 사용되지 않을 때 호출됨const anAtom = atom(1)
anAtom.onMount = (setAtom) => {
console.log('atom is mounted in provider')
setAtom(c => c + 1) // increment count on mount
return () => { ... } // return optional onUnmount function
}
setAtom
함수가 호출되면, atom의 write
가 호출될 것write
를 커스텀 해서 동작을 변경할 수도 있음const countAtom = atom(1)
const derivedAtom = atom(
(get) => get(countAtom),
(get, set, action) => {
if (action.type === 'init') {
set(countAtom, 10)
} else if (action.type === 'inc') {
set(countAtom, (c) => c + 1)
}
}
)
derivedAtom.onMount = (setAtom) => {
setAtom({ type: 'init' })
}
✨ useEffect()
같은 느낌의 로직이 필요할 때 사용하는 것 같기도? 이 부분은 아직 잘 모르겠다...!
Advanced API
Jotai v2 이후로,
read
함수가 2번째 인자로options
를 가지게 되었다!
options.signal
AbortController
를 사용함read
함수 호출)이 시작되기 전에 트리거 됨const readOnlyDerivedAtom = atom(async (get, { signal }) => {
// use signal to abort your function
})
const writableDerivedAtom = atom(
async (get, { signal }) => {
// use signal to abort your function
},
(get, set, arg) => {
// ...
}
)
signal
값은 AbortSignalsignal.aborted
boolean 값을 확인하거나 addEventListener
를 통해 abort
이벤트를 사용할 수 있음fetch
의 경우, 간단하게 signal
을 사용할 수 있음✨ 잘은 모르겠지만... signal을 일으켜서 async 함수를 중단시키는 개념인 것 같다.
+) options.setSelf
까지는 자세히 알 필요 없을듯
useAtom
hook은 state에 있는 atom value를 읽기 위한 것WeakMap
: 키/값 쌍으로, 여기서 키는 반드시 객체 또는 등록되지 않은 심볼이고, 값은 임의의 JavaScript 타입useAtom
hook은 atom value와 update function을 tuple
로 반환함 (React의 useState
처럼)atom()
으로 생성된 atom config를 취함useAtom
을 통해 atom이 사용된 경우에만, 초기값이 state에 저장read
함수가 초기값을 계산하기 위해 호출됨const [value, setValue] = useAtom(anAtom)
setValue
는 해당 atom의 write
함수의 3번째 인자로 전달될 인자 하나만 받음write
함수가 어떻게 구현되어 있는지에 의존함const stableAtom = atom(0)
const Component = () => {
const [atomValue] = useAtom(atom(0)) // This will cause an infinite loop
const [atomValue] = useAtom(stableAtom) // This is fine
const [derivedAtomValue] = useAtom(
useMemo(
// This is also fine
() => atom((get) => get(stableAtom) * 2),
[]
)
)
}
✨ atom을 인자에서 바로 생성해서 넣으면 무한 루프에 빠지는듯...?
useReducer
의 기본 동작임)Signatures
// primitive or writable derived atom
function useAtom<Value, Update>(
atom: WritableAtom<Value, Update>,
options?: { store?: Store }
): [Value, SetAtom<Update>]
// read-only atom
function useAtom<Value>(
atom: Atom<Value>,
options?: { store?: Store }
): [Value, never]
useAtom
hook은 Provider에 저장된 atom value의 값을 읽음useState
처럼 tuple 형식으로 atom value와 updating function 반환atom()
으로 생성된 atom config를 취함useAtom
을 통해 사용될 때, Provider에 초기값이 추가됨How atom dependency works
read
함수가 호출될 때마다 dependencies
와 dependents
가 refresh 됨dependency
이고, A는 B의 dependent
임을 의미함dependency
고, 의존하고 있는 애가 dependent
const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())
read
함수는 atom의 첫 번째 파라미터read
함수를 실행하고, uppercaseAtom
이 textAtom
에 의존하고 있다는 것을 알고 있음textAtom
의 dependents에 uppercaseAtom
을 추가!read
함수를 재실행 할 때(왜냐하면 이것의 dependency인 textAtom
이 업데이트 되었으니까), 이 경우에도 마찬가지로 dependency는 다시 생성됨Atoms can be created on demand
useRef
나 useMemo
hook을 사용하길 원할 것useState
로 저장하거나 다른 atom에 저장할 수 있음useAtomValue
const countAtom = atom(0)
const Counter = () => {
const setCount = useSetAtom(countAtom)
const count = useAtomValue(countAtom)
return (
<>
<div>count: {count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
)
}
useSetAtom
hook과 비슷하지만, useAtomValue
는 read-only atom에 대한 접근을 허용useSetAtom
const switchAtom = atom(false)
const SetTrueButton = () => {
const setCount = useSetAtom(switchAtom)
const setTrue = () => setCount(true)
return (
<div>
<button onClick={setTrue}>Set True</button>
</div>
)
}
const SetFalseButton = () => {
const setCount = useSetAtom(switchAtom)
const setFalse = () => setCount(false)
return (
<div>
<button onClick={setFalse}>Set False</button>
</div>
)
}
export default function App() {
const state = useAtomValue(switchAtom)
return (
<div>
State: <b>{state.toString()}</b>
<SetTrueButton />
<SetFalseButton />
</div>
)
}
useSetAtom()
사용const [, setValue] = useAtom(valueAtom)
이 valueAtom
업데이트 할 때마다 불필요한 리렌더링을 유발하기 때문에)createStore
Provider
를 전달하기 위해 사용됨get
함수, atom values를 세팅하는 ② set
함수, atom changes를 구독하는 ③ sub
함수, 이렇게 3가지 함수를 가지고 있음const myStore = createStore()
const countAtom = atom(0)
myStore.set(countAtom, 1)
const unsub = myStore.sub(countAtom, () => {
console.log('countAtom value is changed to', myStore.get(countAtom))
})
// unsub() to unsubscribe
const Root = () => (
<Provider store={myStore}>
<App />
</Provider>
)
getDefaultStore
const defaultStore = getDefaultStore()
Provider
컴포넌트는, 컴포넌트 서브 트리에 state를 제공함Providers가 유용한 이유
1. 각 sub tree에 다른 state 제공
const SubTree = () => (
<Provider>
<Child />
</Provider>
)
Signatures
const Provider: React.FC<{
store?: Store
}>
store
를 가질 수 있음const Root = () => (
<Provider>
<App />
</Provider>
)
store
prop
store
prop을 optional하게 받을 수 있음const myStore = createStore()
const Root = () => (
<Provider store={myStore}>
<App />
</Provider>
)
useStore
const Component = () => {
const store = useStore()
// ...
}
✨ 알듯... 말듯... atom, useAtom까지는 쉬웠는데 Provider와 Store 개념이 약간 헷갈리는 것 같다 ㅠ_ㅠ