Jotai 사용법

이수빈·2023년 9월 19일
1

React

목록 보기
5/20
post-thumbnail
  • prop drilling => 컴포넌트 합성으로도 해결 가능하군

  • 4가지 Core개념 존재 atom, useAtom, Store, Provider

Provider

  • 먼저 App Component 전체를 Provider로 감싼다
//Jotai Provder.tsx
import type { FC, PropsWithChildren } from 'react';

import { Provider } from 'jotai';

const JotaiProvider: FC<PropsWithChildren> = ({ children }) => {
  return <Provider>{children}</Provider>;
};

export default JotaiProvider;

//_app.tsx

export default function App({ Component, pageProps }: AppPropsWithLayout) {
  return (
    <Provider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </Provider>
  );
  • 사용하는 페이지에서 atom 사용 (기본)

  • 3가지 종류의 atom이 존재한다 => read-only, write-only, read-write atom존재

atom 기본 사용법

  • provider를 감싼후, atom파일생성 => 사용하려는 쪽에서 import후 state처럼 사용함.

  • string key의 부재 => 내부적으로 atom을 추적해서 사용되지 않으면 메모리에서 제거함

//atom.ts
import { atom } from "jotai";

export const countAtom = atom(0);

//todolist.tsx


export default function TodoList() {
  const [name, setName] = useState<String>("");
  const url = "http://localhost:3000/api/hello";
  const [count, setCount] = useAtom(countAtom); // atom기본사용법
  ...
  

atom 심화사용법

useSetAtom, useAtomValue

  • useSetAtom(write), useAtomValue(read)로 각각 atom을 set,get 할 수 있음

  • useSetAtom과 useAtomValue는 상태변경이나 읽기에 의해 리렌더링 되지 않도록 할 수 있음
    => 상황에 맞게 사용 권장.

 const readAtom = useAtomValue(readOnlyAtom);

 const writeAtom = useSetAtom(writeOnlyAtom);

derived Atom(파생된 atom)

  • 기존의 atom값을 가져와 새로운 atom을 만들거나, 기존 atom에 새로운 setter를 생성할 수 있음

  • 3가지의 atom존재함 => Read Only, Write Only, Read and Write

ReadOnly atom

  • get함수를 통해 기존의 atom 값을 가져와 새로운 atom을 반환. 만약 get으로 가져온 atom 값이 변한다면, readOnlyAtom 값 또한 변함.
export const countAtom = atom(0);

function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>

export const readOnlyAtom = atom((get) => get(countAtom) * 2);

WriteOnly atom

  • set함수를 통해 기존의 atom을 변화하는 atom

  • 첫번째 인자는 null로 주는게 관습 => set함수는 첫번째 파라미터로 변경시킬 atom, 두번째 파라미터로 변경시킬값을 받음)

function atom<Value, Update>(
  read: Value,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>

export const writeOnlyAtom = atom(
  null, // it's a convention to pass `null` for the first argument
  (get, set, update: number) => {
    // `update` is any single value we receive for updating this atom
    set(countAtom, get(countAtom) - update);
  }
);

ReadWrite Atom

  • 위에 2개의 통합형
function atom<Value, Update>(
  read: (get: Getter) => Value | Promise<Value>,
  write: (get: Getter, set: Setter, update: Update) => void | Promise<void>
): WritableAtom<Value, Update>


export const readWriteAtom = atom(
  (get) => get(countAtom) * 2,
  //읽기
  (get, set, newPrice: number) => {
    set(countAtom, get(countAtom) - newPrice);
    // 쓰기
  }
);

컴포넌트 내에서 Atom생성

  • 컴포넌트내에서 atom을 선언 할 수도 있다. 리렌더링 방지를 위해 useMemo훅으로 감싸주자.
const Component = ({ value }) => {
  const valueAtom = useMemo(() => atom({ value }), [value])
  // ...
}

onMount 프로퍼티

  • atom은 onMount라는 optional property를 가질 수 있다.

  • onMount는 setAtom을 인자값으로 받고, onUnmount function을 return값으로 가진다.(optional)

  • Provider내에서 atom이 처음으로 사용되었을때 call되고, atom이 Provider에서 사용되지 않을때 onUnmount된다 (근데 page가 변하면서 atom도 다시 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
}

export const countAtom = atom(0);

countAtom.onMount = (setAtom) => {
  setAtom(10);

  return () => {
    setAtom(-1);
  };
};

atom 비동기 처리 지원

  • 비동기처리후 데이터를 전역상태에 저장 가능함.

onst 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) => {
    // ...
  }
)


import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'

const userIdAtom = atom(1)
const userAtom = atom(async (get, { signal }) => {
  const userId = get(userIdAtom)
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`,
    { signal }
  )
  return response.json()
})

const Controls = () => {
  const [userId, setUserId] = useAtom(userIdAtom)
  return (
    <div>
      User Id: {userId}
      <button onClick={() => setUserId((c) => c - 1)}>Prev</button>
      <button onClick={() => setUserId((c) => c + 1)}>Next</button>
    </div>
  )
}

const UserName = () => {
  const [user] = useAtom(userAtom)
  return <div>User name: {user.name}</div>
}

const App = () => (
  <>
    <Controls />
    <Suspense fallback="Loading...">
      <UserName />
    </Suspense>
  </>
)

export default App

atomWithStorage

  • localStorage, Session Storage에 상태를 저장가능
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>
    </>
  )
}

useAtomCallback

  • useAtomCallback안에서 useCallback 호출가능 => 최적화 가능

  • promise를 return하는 function을 반환함.

useAtomCallback(
  callback: (get: Getter, set: Setter, arg: Arg) => Result
): (arg: Arg) => Promise<Result>
  
  const Monitor = () => {
  const [count, setCount] = useState(0)
  const readCount = useAtomCallback(
    
    useCallback((get) => {
      const currCount = get(countAtom)
      setCount(currCount)
      return currCount
    }, [])
  )
  useEffect(() => {
    const timer = setInterval(async () => {
      console.log(await readCount())
    }, 1000)
    return () => {
      clearInterval(timer)
    }
  }, [readCount])
  return <div>current count: {count}</div>
}

selectAtom

  • atom이 객체형태일때 => 따로 나누어서 데이터를 관리하도록 도와줌
const defaultPerson = {
  name: {
    first: 'Jane',
    last: 'Doe',
  },
  birth: {
    year: 2000,
    month: 'Jan',
    day: 1,
    time: {
      hour: 1,
      minute: 1,
    },
  },
}

// Original atom.
const personAtom = atom(defaultPerson)

// person.name의 값이 변경되었다면 리렌더링됨.(object의 참조가 변경되었더라도 리렌더링이 발생함)
const nameAtom = selectAtom(personAtom, (person) => person.name)

// person.birth를 tracking함. => 데이터가 동일하다면 리런더링 되지 않음

const birthAtom = selectAtom(personAtom, (person) => person.birth, deepEquals)

atomFamily

  • 매개변수화된 atom을 생성하는데 사용하는 util함수 => 동적으로 생성되는 여러개의 atom을 간편하게 관리 할 수 있음

  • atomFamily는 초기값을 받아서 atom을 생성하는 함수를 반환함 => 다른 상태의 동일한 atom을 여러개 생성가능


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


import { atomFamily, useAtom } from 'jotai';

const countAtomFamily = atomFamily((id) => (get) => {
  // param으로 전달된 id를 사용하여 상태를 초기화
  const initialValue = get(initialCountAtom); // 다른 atom의 값을 읽어와서 초기화

  return initialValue;
});

function Counter({ id }) {
  const [count, setCount] = useAtom(countAtomFamily(id));

  const increment = () => setCount((prevCount) => prevCount + 1);
  const decrement = () => setCount((prevCount) => prevCount - 1);

  return (
    <div>
      <p>Count ({id}): {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;

ref) 공식문서 : https://jotai.org/docs/core/atom
blog : https://velog.io/@sasha1107/%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-Jotai

profile
응애 나 애기 개발자

0개의 댓글