jotai
provider
jotai - provider
error log
list component for jotai
loop
iteration
local state
client state

Jotai provider

Jotai로 순회할 때 각각의 atom이 독립적인 context를 가져야 할 때 사용할 수 있는 전략입니다.

<li/> 컴포넌트에서 Jotai atom 복제하는 방법

  • Jotai의 atom은 근본적으로 1개입니다. 렉시컬 환경을 활용해서 여러개로 복제하는 것은 단순한 atom으로 불가능합니다.

시도

ChatGPT

import { atom, useAtom } from 'jotai';

// 독립적인 atom을 생성하는 함수
const createIndependentAtom = (initialValue) => atom(initialValue);

// 예시 컴포넌트
const ExampleComponent = () => {
  // 독립적인 atom을 생성
  const independentAtom = createIndependentAtom('initial value');
  // atom의 상태와 업데이트 함수를 가져옴
  const [value, setValue] = useAtom(independentAtom);

  const handleClick = () => {
    // atom의 상태 업데이트
    setValue('new value');
  };

  return (
    <div>
      <p>Atom value: {value}</p>
      <button onClick={handleClick}>Update Atom</button>
    </div>
  );
};

export default ExampleComponent;

이런 응답을 받았습니다.

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),
      []
    )
  );
};

Jotai 공식문서를 보면 무한 리랜더링을 발생시킨다고 합니다.

기대와 결과가 일치하고 무한 리랜더링이 발생했습니다.

구글 검색: jotai list component

Large objects - Jotai 공식문서

공식문서에서 특수한 레시피를 알려줍니다.

일단 동작하는

const initialData = {
  people: [
    {
      name: 'Luke Skywalker',
      information: { height: 172 },
      siblings: ['John Skywalker', 'Doe Skywalker'],
    },
    {
      name: 'C-3PO',
      information: { height: 167 },
      siblings: ['John Doe', 'Doe John'],
    },
  ],
  films: [
    {
      title: 'A New Hope',
      planets: ['Tatooine', 'Alderaan'],
    },
    {
      title: 'The Empire Strikes Back',
      planets: ['Hoth'],
    },
  ],
  info: {
    tags: ['People', 'Films', 'Planets', 'Titles'],
  },
};

하지만 이것은 자세히 보니까 atom을 참조형으로 정의하고 소비하는 것이었습니다.

useMemo

useMemo를 통해서 각각의 컴포넌트마다 렉시컬 환경을 활용하는 방법이 있습니다. 모듈 스코프에서는 하나의 state가 되지만 순회하는 컴포넌트 내부에서 선언하면 동적으로 선언할 수 있다고 합니다.

Note about creating an atom in render function - Jotai 공식문서

const Component = ({ value }) => {
  const valueAtom = useMemo(() => atom({ value }), [value]);
  // ...
};

useMemo, useRef 2가지 모두 활용할 수 있지만 공식 문서는 useMemo를 활용하고 있기 때문에 저도 useMemo를 활용해보겠습니다.

export function Card({ question, answer, _id, stackCount }: Card) {
  // ... 생략
  const activeAtom = useMemo(() => atom(false), []);
  const [active, setActive] = useAtom(activeAtom);

  const editingAtom = useMemo(() => atom(false), []);
  const [isEditing, setIsEditing] = useAtom(editingAtom);
  // ... 생략

일단 다리를 바꾸니까 성공적으로 동작했습니다.

atom을 렉시컬 환경에서 자원공유가 가능해졌습니다.

다음 단계입니다. 이 atom을 공유하는 방법입니다. 처음에는 custom hook을 활용할까? 생각했습니다. 틀린 생각입니다. custom hook을 활용하면 custom hook을 호출할 때 렉시컬 환경을 활용해서 새롭게 atom이 만들어지기 때문에 부적합니다.

렉시컬 환경 단위로 atom을 공유해야 합니다.

다음 생각은 props로 공유하면 card 컴포넌트의 렉시컬 환경을 활용해서 card 내 같은 atom을 공유할 것이라는 생각이 들었습니다. 하지만 아직 생각 짧습니다. provider를 활용하면 card에서 모두 읽고 쓰기가 가능하다는 생각이 들었습니다.

하지만 또 문제가 있습니다. atom은 함수 안에서 정의되는데 어떻게 provider로 공유할 것인가?

해결: provider 활용

Isolate State in an Application with Jotai Provider - Daishi Kato

jotai-tutorial-10 - Daishi Kato

Jotai를 가르치는 튜토리얼입니다. 여기서 provider는 원래 전역상태로 관리하는데 이렇게하면 각각 독립적인 컨텍스트를 갖게 됩니다.

이것을 이해해보면 순회하는 위치에서 provider를 적용하면 된다는 것입니다.

function Cards() {
  // ... 생략
  return (
    <div>
      {/* ... 생략 */}
      <CardContainer>
        {cards.map((card) => (
          // 여기는 provider가 없습니다.
          <Card {...card} key={card._id} />
        ))}
      </CardContainer>
      {/* ... 생략 */}
    </div>
  );
}

export default Cards;

현재 상태입니다.

provider가 현재 없습니다.

import { Provider } from 'jotai';

function Cards() {
  // ... 생략
  return (
    <div>
      {/* ... 생략 */}
      <CardContainer>
        {cards.map((card) => (
          // highlight-start
          // 여기는 Jotai provider로 감쌉니다.
          <Provider>
            <Card {...card} key={card._id} />
          </Provider>
          // highlight-end
        ))}
      </CardContainer>
      {/* ... 생략 */}
    </div>
  );
}

export default Cards;

Jotai provider를 적용하면 동일한 atom을 읽어도 독립적인 context를 갖을 수 있습니다.

또 무조건 atom을 공유할 필요는 없습니다.

const activeAtom = atom(false);
const editingAtom = atom(false);

export function Card({ question, answer, _id, stackCount }: Card) {
  const [active, setActive] = useAtom(activeAtom);
  const [isEditing, setIsEditing] = useAtom(editingAtom);

  // ... 생략

무조건 상위 혹은 custom hook에서 전역으로 공유받을 필요는 없습니다. 하위 모듈의 atom을 주입받을 수 있습니다.

provider 없음

위는 provider가 없습니다.

provider 있음

위는 provider로 감싸져있는 경우입니다.

학습

  • jotai provider를 제공하면 atom은 독립적인 context를 갖을 수 있게 됩니다.