prop drilling => 컴포넌트 합성으로도 해결 가능하군
4가지 Core개념 존재 atom, useAtom, Store, 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존재
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기본사용법
...
useSetAtom(write), useAtomValue(read)로 각각 atom을 set,get 할 수 있음
useSetAtom과 useAtomValue는 상태변경이나 읽기에 의해 리렌더링 되지 않도록 할 수 있음
=> 상황에 맞게 사용 권장.
const readAtom = useAtomValue(readOnlyAtom);
const writeAtom = useSetAtom(writeOnlyAtom);
기존의 atom값을 가져와 새로운 atom을 만들거나, 기존 atom에 새로운 setter를 생성할 수 있음
3가지의 atom존재함 => Read Only, Write Only, Read and Write
export const countAtom = atom(0);
function atom<Value>(read: (get: Getter) => Value | Promise<Value>): Atom<Value>
export const readOnlyAtom = atom((get) => get(countAtom) * 2);
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);
}
);
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);
// 쓰기
}
);
const Component = ({ value }) => {
const valueAtom = useMemo(() => atom({ value }), [value])
// ...
}
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);
};
};
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
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안에서 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>
}
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)
매개변수화된 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