

props drilling🪛
Props Drilling 이란 props 를 하위 컴포넌트로 전달하는 과정에서 몇개의 컴포넌트를 뚫고 들어가는 형태를 의미한다.
props로 전달하는 컴포넌트 수가 적으면 크게 문제가 되지 않지만, 많은 컴포넌트들을 뚫고 state를 전달해야 한다면 그 props를 추적하기 어려워진다.
redux, recoil 등의 전역 상태 관리 라이브러리를 사용하여 이와 같은 props drilling을 방지할 수 있다.

Store라는 상태 저장소를 기반으로, Action 타입을 Reducer에 전달하면 해당 타입에 맞는 동작에 따라 상태값을 갱신
컴포넌트는 Selector를 사용해 Store에서 필요한 상태값을 구독(subscribing)하는 형태
장점 : 단방향 데이터 구조 → 예측 가능한 상태 관리
단점 : 많은 보일러플레이트 코드
Action
Dispatcher
Store
View

전체 상태들을 모아놓고 엑세스를 제공하며, 컴포넌트에서 사용되는 일부 상태를 자동으로 감지하고 업데이트를 인지하는 패턴
장점: store 데이터에 바로 엑세스하여 변경 가능 → 편리성단점: 단순한 패턴인 만큼 디버깅이 어렵다React의 state와 비슷하게, 컴포넌트 트리 안에 상태들이 존재하며 이들이 상향식(bottom-up)으로 수집 및 공유되는 패턴이다.
상태들은 atom이라고 불리는 객체에서 설정하며, 값의 참조와 조작은 React.useState와 유사하게[state, setState]튜플로 수행한다.
Store에서 하향식(top-down)으로 관리되던 기존 패턴과 매우 다르기에, 다른 라이브러리보단 React의 Hooks 및 Context API와 많이 비교된다.
순수함수입니다.


const primitiveAtom = atom(initialValue) // initialValue : atom 값이 변경될 때까지 atom이 반환할 초기값
boolean, number, string, object, array, set, map 등 모든 타입이 될 수 있다.
항상 writable
예시
import { atom } from 'jotai'
const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const animeAtom = atom([
{
title: 'Ghost in the Shell',
year: 1995,
watched: true
},
{
title: 'Serial Experiments Lain',
year: 1998,
watched: false
}
])
다른 atom으로 부터 파생된 atom
derived atom을 생성하려면 read 함수와 optional write 함수를 전달한다.
derived atom의 3가지 패턴
useAtomuseAtomValueuseSetAtomSigniture
Read-Write atom

const derivedAtomWithReadWrite = atom(read, write)
write: 아톰의 값을 변경하는데 사용되는 함수 - useAtom()[1]set: atom config와 new value를 받은 다음 Provider의 atom 값을 업데이트하는 함수update: useAtom()[1]이 반환한 업데이트 함수가 인자로 받은 임의의 값Read-only atom

const derivedAtomWithRead = atom(read)
read: 렌더링할 때 마다 호출되는 함수get: atom config를 받아 프로바이더에 저장된 값을 반환하는 함수Write-only atom

const derivedAtomWithWriteOnly = atom(null, write)
null을 넣어주는 것을 컨벤션으로 함write 함수에서의 get은 읽기 위한 것이지만, 추적되지 않는다. set은 atom 을 write 하기 위한 것이다. 타겟 atom의 write 함수를 호출한다. ⛳ render 함수에서 아톰 생성 시 주의 사항
atom config 는 어디서나 생성할 수 있지만, referential equality가 중요하다. 동적으로 생성할 수도 있다. 렌더 함수에서 아톰을 생성하려면 안정적인 참조를 얻기 위해useMemo나useRef를 사용해야 한다. Memoization을 위해useMemo와useRef중 어떤 것을 사용해야 할지 확실하지 않다면useMemo를 사용해라. 그렇지 않으면useAtom으로 인해 무한 루프가 발생할 수 있다.const Component = ({ value }) => { const valueAtom = useMemo(() => atom({ value }), [value]) // ... }
useAtomuseAtomValueuseSetAtom아톰 값을 읽거나 쓰기만 하는 경우, 리렌더링을 최적화하기 위해 별도의
useAtomValue및useSetAtom훅을 사용합니다.
useAtom// atom.js
import { atom, useAtom } from 'jotai';
const countAtom = atom(0)
const Counter = () => {
const [count, setCount] = useAtom(countAtom)
return (
<>
<div>count: {count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
)
}
useAtom 은 리액트의 useState 와 비슷하게 사용 가능하고 값과 update function로 구성된 튜플로 전달된다.useAtom을 통해 atom이 사용된 후에만 initial value가 state에 저장된다. 만약 그 atom이 derived atom이라면, read 함수가 호출되어 초기값을 계산한다.const [value, setValue] = useAtom(anAtom)useAtomValueimport { atom, useAtomValue } from 'jotai';
const countAtom = atom(0)
const Counter = () => {
const count = useAtomValue(countAtom)
return (
<>
<div>count: {count}</div>
</>
)
}
useAtomValueuseSetAtomimport { atom, useSetAtom } from 'jotai';
const countAtom = atom(0)
const Counter = () => {
const setCount = useSetAtom(countAtom)
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
)
}
Map & WeakMapMap
key와 value로 이루어지는 자료구조
WeakMap
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
const darkModeAtom = atomWithStorage('darkMode', false)
const Page = () => {
const [darkMode, setDarkMode] = useAtom(darkModeAtom)
return (
<>
<h1>Welcome to {darkMode ? 'dark' : 'light'} mode!</h1>
<button onClick={() => setDarkMode(!darkMode)}>toggle theme</button>
</>
)
}

로컬스토리지나 세션스토리지에 상태를 저장하고 사용할 수 있다.
모든 아톰은 비동기 읽기 또는 비동기 쓰기와 같은 비동기 동작을 지원한다.
loadable
- async atom들이 suspend 되거나 에러 바운더리로 throw되는 것을 원하지 않는 경우 loadable 유틸을 사용할 수 있다.
사용 방법: loadable 유틸로 atom을 감싼다.
이 함수는 loading / hasData / hasError 세 가지 상태 중 하나를 반환한다.
import { loadable } from "jotai/utils"
const asyncAtom = atom(async (get) => ...)
const loadableAtom = loadable(asyncAtom)
// Does not need to be wrapped by a <Suspense> element
const Component = () => {
const [value] = useAtom(loadableAtom)
if (value.state === 'hasError') return <Text>{value.error}</Text>
if (value.state === 'loading') {
return <Text>Loading...</Text>
}
console.log(value.data) // Results of the Promise
return <Text>Value: {value.data}</Text>
}
import { useAtom } from 'jotai'
import { atomWithImmer } from 'jotai-immer'
const countAtom = atomWithImmer(0)
const Counter = () => {
const [count] = useAtom(countAtom)
return <div>count: {count}</div>
}
const Controls = () => {
const [, setCount] = useAtom(countAtom)
// setCount === update : (draft: Draft<Value>) => void
const inc = () => setCount((c) => (c = c + 1))
return <button onClick={inc}>+1</button>
}
import { atom, useAtom } from 'jotai'
import { atomsWithQuery } from 'jotai-tanstack-query'
const idAtom = atom(1)
const [userAtom] = atomsWithQuery((get) => ({
queryKey: ['users', get(idAtom)],
queryFn: async ({ queryKey: [, id] }) => {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
return res.json()
},
}))
const UserData = () => {
const [data] = useAtom(userAtom)
return <div>{JSON.stringify(data)}</div>
}
atomsWithQuery for QueryObserveratomsWithInfiniteQuery for InfiniteQueryObserveratomsWithMutation for MutationObserver 그 외에도
라이브러리와의 Integration을 지원한다.
참고자료