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고, 의존하고 있는 애가 dependentconst 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 개념이 약간 헷갈리는 것 같다 ㅠ_ㅠ