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
공식문서에서 특수한 레시피를 알려줍니다.
일단 동작하는
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로 감싸져있는 경우입니다.
학습
- jotai provider를 제공하면 atom은 독립적인 context를 갖을 수 있게 됩니다.