[Jotai 공식문서 정리] 🆙React 상태관리 도구 Jotai Core👻

혜혜·2023년 11월 4일
0

Jotai

목록 보기
2/2
post-thumbnail

Jotai 공식 문서에서 Core 라이브러리에 대한 내용을 조금 더 정리해 보려고 한다!!

Jotai 공식 문서

Core

👻 atom

  • atom() 함수는 atom config를 생성함
  • 이를 'atom config'라고 부르는 이유는, 단지 정의일 뿐, 아직 값을 보유하고 있지 않기 때문
  • 문맥이 명확하다면 그냥 'atom'이라고 부르기도 함
  • atom config는 immutable(불변) object
  • atom config 객체에는 값이 없고, atom value는 store에 존재함
  • primitive 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를 만들 수 있음

    1. Read-only atom
    2. Write-only atom
    3. Read-Write atom
  • derived 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
  }
)
  • read function을 제공할 때는, 인자에 파라미터가 get인 함수를 넣는 게 컨벤션이고,
    write function을 제공할 때는, 첫 번째 인자에 null을 넣고, 두 번째 인자에 파라미터가 get, set, update인 함수를 넣는 게 컨벤션인듯
  • update 변수의 이름은 달라져도 됨
  • read & write 함수를 넣으려면 위 2개를 약간 섞는 듯
  • read function에 있는 get은 atom value를 읽기 위함
    반응형이며, 읽기 dependency가 추적됨
  • write function에 있는 get은 마찬가지로 atom value를 읽기 위함이지만, 추적되지는 않음!
    게다가, Jotai v1 API에 있는 resolve 되지 않은 async value를 읽을 수 없다.
  • write function에 있는 set은 atom value를 쓰기 위함
    이것은 target atom에 있는 write funciton을 호출할 것

Note about creating an atom in render function

  • Atom configs는 어디에서는 생성될 수 있지만, Referential equality(참조 비교)는 중요함
  • 동적으로 생성될 수도 있음
  • render function에서 atom을 생성하기 위해, useMemouseRef는 안정적인 참조를 위해 필요함
  • memoization을 위해 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에 저장된 값을 반환하는 함수
    • dependency은 추적되므로, atom에 대해 get을 1번 이상 사용하면, atom value가 변경될 때마다 read가 재평가됨
  • write : atom value를 변경하는 데 주로 사용되는 함수

    • 반환된 useAtom 쌍의 2번째 값인 useAtom() 호출할 때마다 호출됨
    • primitive atom에 있는 이 함수의 기본값은, 해당 atom의 값을 변경함
    • write의 signature는 (get, set, upgrade) => void | Promise<void>
    • getread에서 설명한 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)
  • atoms에는 ① writable atom과 ② read-only atom 2가지가 있음
  • Primitive atoms항상 writable
  • Derived atomswrite가 명시되어 있으면 writable
  • primitive atoms의 writeReact.useStatesetState와 동등함

debugLabel property

  • 생성된 atom config는 optional property인 debugLabel을 가짐
  • Debug Label은 디버깅에서 atom을 볼 때 사용됨
    Debugging guide
  • Debug Label은 고유할 필요까지는 없지만, 일반적으로 구별 가능하게 만드는 게 좋다

onMount property

  • 생성된 atom config는 optional property인 onMount를 가짐
  • onMountsetAtom 함수를 취하고, optional하게 onUnmount 함수를 리턴하는 함수
  • onMount 함수는 provider에서 atom이 처음으로 사용될 때 호출되며, onUnmount 함수는 그것이 더 이상 사용되지 않을 때 호출됨
  • 일부 극단적인 경우에는, atom을 unmount 한 후에 즉시 mount 할 수도 있음
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

  • async 함수를 중단할 수 있도록 AbortController를 사용함
    AbortController
    ✨ 이런 기능은 처음 알았다...
  • Abort는 새로운 계산(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 값은 AbortSignal
  • signal.aborted boolean 값을 확인하거나 addEventListener를 통해 abort 이벤트를 사용할 수 있음
  • fetch의 경우, 간단하게 signal을 사용할 수 있음

✨ 잘은 모르겠지만... signal을 일으켜서 async 함수를 중단시키는 개념인 것 같다.

+) options.setSelf 까지는 자세히 알 필요 없을듯

👻 useAtom

  • useAtom hook은 state에 있는 atom value를 읽기 위한 것
  • state는 atom configs 및 atom values의 WeakMap으로 볼 수 있음
    WeakMap : 키/값 쌍으로, 여기서 키는 반드시 객체 또는 등록되지 않은 심볼이고, 값은 임의의 JavaScript 타입
  • useAtom hook은 atom value와 update function을 tuple로 반환함 (React의 useState처럼)
  • 이것은 atom()으로 생성된 atom config를 취함
  • 처음에는 atom과 관련된 값이 없었음
  • useAtom을 통해 atom이 사용된 경우에만, 초기값이 state에 저장
  • derived atom의 경우, read 함수가 초기값을 계산하기 위해 호출됨
  • atom이 더 이상 사용되지 않으면 이를 사용하는 모든 컴포넌트가 unmount 되고, atom config가 더 이상 존재하지 않으며, state의 값은 가비지 컬렉팅 됨 (메모리 회수)
const [value, setValue] = useAtom(anAtom)
  • setValue는 해당 atom의 write 함수의 3번째 인자로 전달될 인자 하나만 받음
  • 이 행위는 write 함수가 어떻게 구현되어 있는지에 의존함
  • atom의 reference를 잘 처리하지 않으면 무한 루프에 빠질 수 있으니 주의해야 한다!!
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을 인자에서 바로 생성해서 넣으면 무한 루프에 빠지는듯...?

  • React는 컴포넌트 호출을 담당한다는 점을 기억하자!
  • 즉, 멱등성이 있어야 하고, 여러 번 호출할 준비가 되어 있어야 함
    멱등성 : 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질
  • 어떤 props나 atoms도 변하지 않을 때도, 추가로 리렌더링 되는 경우가 조종 있음...
  • 커밋 없이 추가로 리렌더링되는 것은 예상되는 동작임 (실제로 React 18의 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를 취함
  • 초기에는 Provider에 저장된 값 없음
  • 처음 atom이 useAtom을 통해 사용될 때, Provider에 초기값이 추가됨

How atom dependency works

  • 현재 구현에서는 read 함수가 호출될 때마다 dependenciesdependents가 refresh 됨
  • 예를 들어, 만약 A가 B에 의존하고 있다면, 이것은 B가 A의 dependency이고, A는 B의 dependent임을 의미함
    ✨ 즉, 의존되어지는 애가 dependency고, 의존하고 있는 애가 dependent
const uppercaseAtom = atom((get) => get(textAtom).toUpperCase())
  • read 함수는 atom의 첫 번째 파라미터
  • dependency는 초기에 비어 있음
  • 첫 사용 때, 우리는 read 함수를 실행하고, uppercaseAtomtextAtom에 의존하고 있다는 것을 알고 있음
  • 그래서 textAtom의 dependents에 uppercaseAtom을 추가!
  • 우리가 read 함수를 재실행 할 때(왜냐하면 이것의 dependency인 textAtom이 업데이트 되었으니까), 이 경우에도 마찬가지로 dependency는 다시 생성됨
  • 우리는 그 다음 오래된 dependents를 제거하고, 최신 항목으로 교체함

Atoms can be created on demand

  • 예시에서는 컴포넌트 외부에서 전역적으로 atom을 정의하는 것을 보여주지만, atom을 생성할 수 있는 위치와 시기에 대해서는 제한이 없다!
  • atom이 그들의 object referentail identity로 식별된다는 점을 기억하는 한 언제든지 atom 생성 가능
  • 만약 render 함수에서 atoms를 생성했다면, 아마 memoization을 위해 useRefuseMemo hook을 사용하길 원할 것
  • 아니라면, atom은 component가 렌더링 될 때마다 재생성됨
  • atom을 생성하고 useState로 저장하거나 다른 atom에 저장할 수 있음
    issue #5
  • atoms를 전역적으로 특정 위치에 캐싱할 수 있음
    example1
    example2
  • 파라미터화된 atom에 대해서는 utilities에서 atomFamily 확인

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과 비슷하지만, useAtomValueread-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>
  )
}
  • atom 값을 읽지 않고 update 해야 할 필요가 있을 때, useSetAtom() 사용
  • 이것은 특히 성능이 중요할 때 유용함 (const [, setValue] = useAtom(valueAtom)valueAtom 업데이트 할 때마다 불필요한 리렌더링을 유발하기 때문에)

👻 Store

createStore

  • 새로운 빈 store 생성
  • Store는 Provider를 전달하기 위해 사용됨
  • Store는 atom values를 가져오는 ① 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

  • provider-less 모드에서 사용되는 기본 store 반환
const defaultStore = getDefaultStore()

👻 Provider

  • Provider 컴포넌트는, 컴포넌트 서브 트리에 state를 제공함
  • 다수의 Providers가 다수의 subtress에 사용될 수 있고, 중첩될 수도 있음
  • React Context처럼 동작한다!
  • atom이 Provider 없이 tree에서 사용되면, 기본 state를 사용할 것
    → 소위 provider-less 모드!
  • Providers가 유용한 이유

    	1. 각 sub tree에 다른 state 제공
    1. atoms의 초기값 accept
    2. remounting을 통해 모든 atoms 제거
const SubTree = () => (
  <Provider>
    <Child />
  </Provider>
)

Signatures

const Provider: React.FC<{
  store?: Store
}>
  • atom configs는 values를 가지고 있지 않음
  • atom values는 별도의 stores에 있는 것
  • Provider는 store를 포함하고 component tree 아래에 atom values를 제공하는 컴포넌트
  • Provider는 React context의 provider처럼 동작함
  • 만약 Provider를 사용하지 않으면, default store와 함께 provider-less 모드로 동작함
  • Provider는 다른 components tress를 위해 다른 atom values를 가지고 있어야 할 때 필수적임
  • Provider는 optional한 prop으로 store를 가질 수 있음
const Root = () => (
  <Provider>
    <App />
  </Provider>
)

store prop

  • Provider는 Provider subtree를 위해 사용할 수 있는 store prop을 optional하게 받을 수 있음
const myStore = createStore()

const Root = () => (
  <Provider store={myStore}>
    <App />
  </Provider>
)

useStore

  • 이 hook은 component tree 안에 store를 리턴
const Component = () => {
  const store = useStore()
  // ...
}

✨ 알듯... 말듯... atom, useAtom까지는 쉬웠는데 Provider와 Store 개념이 약간 헷갈리는 것 같다 ㅠ_ㅠ

profile
쉽게만살아가면재미없어빙고

0개의 댓글