jotai 톺아보기

soyoung kim·2023년 10월 12일
0

atom

atom 함수는 atom 구성을 생성하는 함수입니다. 정의일 뿐이며 아직 값을 보유하지 않기 때문에 "atom config"라고 부릅니다. 문맥이 명확하다면 그냥 "atom"이라고 부를 수도 있습니다.

아톰 구성은 불변 객체입니다. atom 구성 객체는 값을 보유하지 않습니다. 원자 값은 스토어에 존재합니다.

원시 atom(config)을 생성하려면 초기 값만 제공하면 됩니다.

import { atom } from 'jotai'

const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

파생 atom을 생성할 수도 있습니다. 세 가지 패턴이 있습니다:

  • 읽기 전용 원자
  • 쓰기 전용 원자
  • 읽기-쓰기 원자

파생된 atom을 생성하기 위해 읽기 함수와 선택적 쓰기 함수를 전달합니다.

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
  }
)

Atom은 쓰기 가능한 atom와 읽기 전용 atom의 두 가지 종류가 있습니다. 프리미티브 atom은 항상 쓰기 가능합니다. 파생된 atom의 쓰기가 지정되면 쓰기 가능합니다. 프리미티브 atom의 쓰기는 React.useState의 setState와 동일합니다.

onMount property

생성된 아톰 구성은 선택적 속성인 onMount를 가질 수 있습니다. onMount는 setAtom 함수를 취하고 선택적으로 onUnmount 함수를 반환하는 함수입니다.

공급자에서 아톰이 처음 사용될 때 onMount 함수가 호출되고, 더 이상 사용되지 않을 때 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 함수를 호출하면 아톰의 쓰기가 호출됩니다. 쓰기를 커스터마이징하면 동작을 변경할 수 있습니다.

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' })
}

Storage

atomWithStorage

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>
    </>
  )
}

atomWithStorage 함수는 React의 경우 localStorage 또는 세션스토리지, React Native의 경우 AsyncStorage에 지속되는 값을 가진 아톰을 생성합니다.

Server-side rendering

저장된 원자 값에 의존하는 모든 JSX 마크업(예: className 또는 스타일 프로퍼티)은 서버에서 렌더링될 때 initialValue를 사용합니다(서버에서 localStorage 및 sessionStorage를 사용할 수 없으므로).

즉, 사용자의 브라우저에 원래 HTML로 제공되는 것과 재수화(rehydration) 과정에서 React가 예상하는 것 사이에 불일치가 발생할 수 있으며, 사용자가 initialValue와 다른 storedValue를 가지고 있는 경우 이 불일치가 발생하게 됩니다.

이 문제에 대한 제안된 해결 방법은 재수화 후에만 렌더링하는 <ClientOnly> 래퍼로 래핑하여 저장된 값 클라이언트 측에 종속된 콘텐츠만 렌더링하는 것입니다. 다른 해결책도 기술적으로 가능하지만 초기값이 저장된 값으로 바뀔 때 잠깐의 "깜박임"이 발생하여 사용자 경험이 좋지 않을 수 있으므로 이 해결 방법을 권장합니다.

function ClientOnly({ children, ...delegated }) {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  if (!hasMounted) {
    return null;
  }
  return (
    <div {...delegated}>
      {children}
    </div>
  );
}

Validating stored values

스토리지 아톰에 런타임 유효성 검사를 추가하려면 스토리지의 사용자 정의 구현을 만들어야 합니다.

아래는 Zod를 사용하여 크로스 탭 동기화를 통해 localStorage에 저장된 값의 유효성을 검사하는 예제입니다.

import { atomWithStorage, createJSONStorage } from 'jotai/utils'
import { z } from 'zod'

const myNumberSchema = z.number().int().nonnegative()

const storedNumberAtom = atomWithStorage('my-number', 0, {
  getItem(key, initialValue) {
    const storedValue = localStorage.getItem(key)
    try {
      return myNumberSchema.parse(JSON.parse(storedValue ?? ''))
    } catch {
      return initialValue
    }
  },
  setItem(key, value) {
    localStorage.setItem(key, JSON.stringify(value))
  },
  removeItem(key) {
    localStorage.removeItem(key)
  },
  subscribe(key, callback, initialValue) {
    if (
      typeof window === 'undefined' ||
      typeof window.addEventListener === 'undefined'
    ) {
      return
    }
    window.addEventListener('storage', (e) => {
      if (e.storageArea === localStorage && e.key === key) {
        let newValue
        try {
          newValue = myNumberSchema.parse(JSON.parse(e.newValue ?? ''))
        } catch {
          newValue = initialValue
        }
        callback(newValue)
      }
    })
  },
})

Family

atomFamily(initializeAtom, areEqual): (param) => Atom

매개변수를 받아 아톰을 반환하는 함수를 생성합니다. 이미 생성된 경우 캐시에서 반환합니다. 초기화아톰은 모든 종류의 아톰을 반환할 수 있는 함수입니다(atom(), atomWithDefault(), ...). 두 매개변수가 동일한지 여부를 알려주는 areEqual은 선택 사항입니다(기본값은 Object.is).

Caveat: Memory Leaks

내부적으로 atomFamily는 키가 매개변수이고 값이 아톰 구성인 Map일 뿐입니다. 사용하지 않는 매개변수를 명시적으로 제거하지 않으면 메모리 누수가 발생합니다. 이는 무한한 수의 파라미터를 사용하는 경우 매우 중요합니다.

myFamily.remove(param)를 사용하면 특정 파라미터를 제거할 수 있습니다.
myFamily.setShouldRemove(shouldRemove)는 캐시에서 아톰을 가져올 때 즉시 실행되는 shouldRemove 함수를 등록하는 것입니다.
shouldRemove는 두 개의 인자 createdAt(밀리초 단위)와 param을 받아 부울 값을 반환하는 함수입니다.
null로 설정하면 이전에 등록된 함수가 제거됩니다.

const someAtomFamily = atomFamily((num) => atom(num));
const anAtom = someAtomFamily(1) // creates a new atom
someAtomFamily.remove(1) // removes it
// simplified code
const atomFamily = (initializeAtom) => {
  const cache = new Map();
  return (param) => {
    if (!cache.has(param)) {
      cache.set(param, initializeAtom(param));
    }
    return cache.get(param);
  };
};

atomFamily 사용 예시

import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'

const todoFamily = atomFamily((name) => atom(name))

todoFamily('foo')
// this will create a new atom('foo'), or return the one if already created

jotai 가 context API의 완벽한 대체제가 될까 ?
https://github.com/pmndrs/jotai/discussions/973

profile
Front-end Developer

0개의 댓글