Jotai
2023-08-03
ReactJotaiState-management
Table of Contents
- 1. 프로젝트 세팅 React + TS + Vite
- 2. 사용해보기
1. 프로젝트 세팅 React + TS + Vite
$ npm init vite@latest React-Jotai -- --template react-ts
$ cd React-Jotai
$ npm i
$ npm i jotai
2. 사용해보기
2-1. 카운터 만들기
- 기본적인 atom 사용 방법을 알아봅니다.
Atom 만들고, 출력하기
/src/atoms/counterAtoms.ts
import { atom } from "jotai";
const counterAtom = atom(0); //initial state
export { counterAtom }
- atom을 export 해줍니다. counterAtom은 전역적으로 접근할 수 있습니다. /src/components/Counter.tsx
import { useAtom } from "jotai"; // 설정한 atom을 쓰려면 가져와야 합니다.
import { counterAtom } from "../atoms/counterAtoms"; // 설정한 atom을 가져옵니다.
const Counter = () => {
const [count, setCount] = useAtom(counterAtom); // initial state로 받은 값을 useAtom에 넣어주면 현재 value와 setValue가 출력된다.
return (
<div>
<div>{count}</div>
</div>
);
};
export default Counter;
- React의 useState와 똑같습니다. useAtom이 리턴하는 배열의 첫 번째 항목은 counterAtom의 설정한 값이되고, setCount는 설정한 값을 변경할 수 있습니다.
/src/App.tsx
import Counter from "./components/Counter";
function App() {
return (
<main>
<Counter />
</main>
);
}
export default App;
- App.tsx 에 추가해줍니다.
Atom 값 변경하기
/src/components/Counter.tsx

import { useAtom } from "jotai"; // 설정한 atom을 쓰려면 가져와야 합니다.
import { counterAtom } from "../atoms/counterAtoms"; // 설정한 atom을 가져옵니다.
const Counter = () => {
const [count, setCount] = useAtom(counterAtom); // initial state로 받은 값을 useAtom에 넣어주면 현재 value와 setValue가 출력된다.
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((prev) => prev + 1)}>One Up</button>
</div>
);
};
export default Counter;
- 버튼 하나를 넣어보겠습니다. onClick시 counterAtom의 값이 변경되게 해봤습니다.
- useState의 문법이랑 다른게 없습니다. 친숙한 문법이죠.
ReadOnly Atom 설정해보기
- 읽기만 가능한 Atom을 설정할 수 있습니다.
/src/atoms/counterAtoms.ts
import { atom } from "jotai";
const counterAtom = atom(0); //initial state
const readOnlyDoubleCounterAtom = atom((get) => get(counterAtom) * 2);
export { counterAtom, readOnlyDoubleCounterAtom }
- get 함수로 다른 atom 값을 할당할 수 있습니다.
- 현재 get으로 부르고 있는 readOnlyDoubleCounterAtom의 type을 보면
Atom<number>인데, atom 주입하는 값의 타입에 따라 같은 useAtom이어도 리턴하는 객체가 다릅니다.
ReadOnly Atom 사용해보기
/src/component/Counter.tsx

import { useAtom } from "jotai"; // 설정한 atom을 쓰려면 가져와야 합니다.
import { counterAtom, readOnlyDoubleCounterAtom } from "../atoms/counterAtoms"; // 설정한 atom을 가져옵니다.
const Counter = () => {
const [count, setCount] = useAtom(counterAtom); // initial state로 받은 값을 useAtom에 넣어주면 현재 value와 setValue가 출력됩니다.
const [doubledCount] = useAtom(readOnlyDoubleCounterAtom); //readonly
return (
<div>
<div>{count}</div>
<button onClick={() => setCount((prev) => prev + 1)}>One Up</button>
<hr />
<h4>Double!</h4>
<h2>{`read only ${doubledCount}`}</h2>
</div>
);
};
export default Counter;
- readOnlyDoubleCounterAtom을 가져와서 useAtom으로 할당합니다.
- ReadOnly atom은 배열의 첫 번째 값에 담겨져있습니다. 값이 무조건 couterAtom의 값보다 있다 * 2만큼 증가한 수치만큼 표현되어야 합니다. (One Up버튼을 누릅니다)
WriteOnly Atom 설정해보기
- 쓰기만 가능한 Atom을 설정할 수 있습니다.
/src/atoms/counterAtoms.ts
import { atom } from "jotai";
const counterAtom = atom(0); //initial state
const writeOnlyThreeCounterAtom = atom(null, (get, set) => set(counterAtom, 3)); // 쓰기만 가능한 value를 만들 수 있다.
export { counterAtom, writeOnlyThreeCounterAtom }
- set 함수로 atom 값을 할당할 수 있습니다. WriteOnly atom 함수의 첫번째 인자는 null로 넣어줍시다.
- 두번째 인자의 파라미터는 세개입니다. (get, set, args) args는 set 함수에게 전해줄 값을 넣어줄 수 있습니다. 예제의 코드에선 사용하지 않았습니다.
WriteOnly Atom 사용해보기
/src/component/Counter.tsx

import { useAtom } from "jotai"; // 설정한 atom을 쓰려면 가져와야 합니다.
import { counterAtom, writeOnlyThreeCounterAtom } from "../atoms/counterAtoms"; // 설정한 atom을 가져옵니다.
const Counter = () => {
const [count, setCount] = useAtom(counterAtom); // initial state로 받은 값을 useAtom에 넣어주면 현재 value와 setValue가 출력됩니다.
const [, setThree] = useAtom(writeOnlyThreeCounterAtom); //writeonly
return (
<div>
<div>{count}</div>
<hr />
<div>
<button onClick={setThree}>Set Three!</button>
</div>
</div>
);
};
export default Counter;
- writeOnlyThreeCounterAtom을 가져와서 useAtom으로 할당합니다.
- WriteOnly atom은 배열의 두 번째 값에 set으로 설정한 함수가 담겨져있습니다. couterAtom의 onClick 시 값이 3으로 설정됩니다.
Read-Write Atom 설정해보기
- 읽고, 쓰기가 가능한 Atom을 사용할 수 있습니다. 그냥 쉽게 생각하면 useState의 값과 set 변경함수를 세팅해 준다 생각하면 쉽습니다.
/src/atoms/counterAtom.ts
import { atom } from "jotai";
const counterAtom = atom(0); //initial state
//readWrite Atom
const decrementCountAtom = atom(
(get) => get(counterAtom), // get 함수는 기존의 atom 값을 읽을 수 있는 함수이고,
(get, set) => set(counterAtom, get(counterAtom) - 1) // set함수는 기존의 atom 의 값을 변화시킬 수 있는 함수이다.
// _arg 는 이후 set함수의 인자값을 의미한다. (현재 이 예시에선 사용되지 않음)
);
export {
counterAtom,
decrementCountAtom,
};
- get으로 counterAtom으로 useAtom의 배열의 첫 번째 순서에 올 값을 설정합니다.
- set으로 counterAtom으로 useAtom의 배열의 두 번째 순서에 올 값을 설정합니다.
Read-Write Atom 사용해보기
/src/components/Counter.tsx

import { useAtom } from "jotai";
import {
counterAtom,
decrementCountAtom,
} from "../atoms/counterAtoms";
const Counter = () => {
const [count, setCount] = useAtom(counterAtom); // initial state로 받은 값을 useAtom에 넣어주면 현재 value와 setValue가 출력된다.
const [decrementCount, decrement] = useAtom(decrementCountAtom); //get, set으로 설정한 값이 들어간다.
return (
<div>
<div>{count}</div>
<hr />
<div>{decrementCount}</div>
<button onClick={decrement}>One Down!</button>
</div>
);
};
export default Counter;
- get, set으로 설정한 값이 들어가고, useState랑 같은 문법으로 동작합니다.
2-2. 비동기
비동기 Atom 설정해보기
/src/atoms/fetchAtoms.ts
import { atom } from "jotai";
const fetchDummyData = atom(async () => {
try {
const result = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const data = await result.json();
return data;
} catch (error) {
console.log(error);
}
});
export { fetchDummyData };
- 데이터는 jsonplace데이터를 가져왔습니다. atom안에 간단히 async Function을 추가할 수 있습니다.
비동기 Atom 사용해보기
/src/components/Fetch.tsx
import { fetchDummyData } from "../atoms/fetchAtoms";
import { useAtom } from "jotai";
const Fetch = () => {
// fetch data
const [fetchData] = useAtom(fetchDummyData);
if (!fetchData) return;
return (
<div>
<h3>userID: {fetchData.userId}</h3>
<h3>title: {fetchData.title}</h3>
<h3>completed: {`${fetchData.completed}`}</h3>
<h3>id: {fetchData.id}</h3>
</div>
);
};
export default Fetch;
- 간단히 표현할 수 있습니다.
번외) 동적 Atom으로 서버 데이터 호출하기
/src/atoms/fetchAtoms.ts
import { atom } from "jotai";
const userIdAtom = atom(1);
const fetchUserIdAtom = atom(async (get) => {
const userId = get(userIdAtom);
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`
);
return response.json();
});
export { fetchDummyData };
- 통신할 URL에 get함수로 userIdAtom을 가져와 할당합니다. 약 2초의 시간 이상이 걸리게 설정하였습니다.
비동기 Atom 사용해보기
/src/components/Fetch.tsx
import { fetchUserIdAtom, userIdAtom } from "../atoms/fetchAtoms";
import { useAtom } from "jotai";
const Fetch = () => {
// fetch data
const [userID, setUserID] = useAtom(userIdAtom);
const [fetchUserID] = useAtom(fetchUserIdAtom);
return (
<div>
User Id: {userID}
<button onClick={() => setUserID((c) => c - 1)}>Prev</button>
<button onClick={() => setUserID((c) => c + 1)}>Next</button>
<div>{JSON.stringify(fetchUserID)}</div>
</div>
);
};
export default Fetch;
- 버튼을 누르면 userIdAtom이 변하고, 구독하고 있는 fetchUserIdAtom이 변화하면서 동적인 데이터를 서버에 요청하게 됩니다.
Loading시 예외처리
/src/App.tsx
import Fetch from "./components/Fetch";
import { Suspense } from "react";
function App() {
return (
<main>
{/* 비동기 수행 중일때 예외처리 */}
<Suspense fallback='Loading...'>
<Fetch />
</Suspense>
</main>
);
}
export default App;
- React.Suspense로 데이터를 받아오기 전에 예외적 처리를 선언해줍니다.
2-3. mount
- Atom이 처음으로 불려졌을때 onMount 메소드로 값을 설정할 수 있습니다.
mount 설정해보기
src/atoms/mountAtoms.ts
import { atom } from "jotai";
type ActionType = { type: "init" | "inc" | undefined };
const mountAtom = atom(1); //initial state
const derivedAtom = atom(
(get) => get(mountAtom),
(get, set, action: ActionType) => {
if (action.type === "init") {
set(mountAtom, 10);
} else if (action.type === "inc") {
set(mountAtom, (current: number) => {
return current + 1;
});
} else {
set(mountAtom, 100);
}
}
);
derivedAtom.onMount = (setAtom) => { //useEffect를 대체할 수 있어 useEffect의 예상치 못한 사이드 이펙트를 방지
//atom이 처음 불려질 시점에 할당할 set 로직 작성
setAtom({ type: "init" });
return () => {
setAtom({ type: undefined });
};
};
export { mountAtom, derivedAtom };
- derivedAtom.onMount로 불러질 시 값을 설정하게 해봤습니다. return 함수로 unMount 시 설정할 값을 할당하는 것도 가능합니다.
- 뭔가 useEffect랑 같은 문법입니다. useEffect로 할 수 있는 동작들을 설정 할 수 있습니다.
mount 사용해보기
/src/components/Mount.tsx
import {
mountAtom,
derivedAtom
} from "../atoms/onMountAtoms";
import { useAtom } from "jotai";
const Mount = () => {
const [mount] = useAtom(mountAtom);
const [derived, setDerived] = useAtom(derivedAtom);
return (
<div>
<div>
<h4>
mountAtom : {mount}
</h4>
</div>
<div>
derivedAtom : {derived}
</div>
<button
onClick={() => {
setDerived({ type: "inc" });
}}>
1 plus
</button>
</div>
);
};
export default Mount;
- mountAtom은 1로 할당되어 있었지만, derivedAtom의 onMount가 실행되어 set으로 설정한 로직이 실행되고, 결국 moutAtom의 값이 변경됩니다.
번외) mount 시 fetch 함수 실행하기
mount 설정해보기
src/atoms/mountAtoms.ts
import { atom } from "jotai";
type ActionType = { type: "init" | "inc" | undefined };
const mountAtom = atom<string>(""); //async initial state
const fetchInitData = async () => {
const data = await fetch("https://jsonplaceholder.typicode.com/todos/1");
return await data.json();
};
const fetchIncData = async () => {
const data = await fetch("https://jsonplaceholder.typicode.com/todos/2");
return await data.json();
};
const derivedAsyncAtom = atom(
(get) => get(mountAtom),
async (get, set, action: ActionType) => {
if (action.type === "init") {
const result = await fetchInitData();
set(mountAtom, JSON.stringify(result));
} else if (action.type === "inc") {
const result = await fetchIncData();
set(mountAtom, JSON.stringify(result));
} else {
set(mountAtom, "ㅜㅜ");
}
}
);
derivedAsyncAtom.onMount = (setAtom) =>
setAtom({ type: "init" });
return () => {
setAtom({ type: undefined });
};
};
export {
mountAtom,
derivedAsyncAtom,
};
- fetch된 데이터를 받아올 함수 2개를 설정합니다.
- onMount 실행 시 set 설정된 함수가 실행되고, set함수에 설정된 fetch 함수가 실행되어 Atom을 처음으로 호출 할 시 fetch된 데이터를 부르고 mountAtom에 할당합니다.
mount 사용해보기
/src/components/Mount.tsx
import {
mountAtom,
derivedAsyncAtom,
} from "../atoms/onMountAtoms";
import { useAtom } from "jotai";
const Mount = () => {
const [mount] = useAtom(mountAtom);
const [derived, setDerived] = useAtom(derivedAsyncAtom);
return (
<div>
<div>
<h4>mountAtom : {`${mount}`}</h4>
</div>
<div>derivedAsyncAtom : {`${derived}`}</div>
<button
onClick={() => {
setDerived({ type: "inc" });
}}>
fetch!
</button>
</div>
);
};
export default Mount;
- mountAtom, derivedAsyncAtom의 데이터를 출력해봅니다.
2-4. Atom 값 LocalStorage에 저장하기
/src/atoms/darkMode.ts
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
const darkModeAtom = atomWithStorage("darkMode", false);
//writeonly mode
const darkModeOff = atom(null, (get, set) => {
set(darkModeAtom, false);
});
const darkModeOn = atom(null, (get, set) => {
set(darkModeAtom, true);
});
export { darkModeAtom, darkModeOff, darkModeOn };
- set함수로 darkModeAtom의 값을 변경해보았습니다. atomWithStorage를 쓰면 atom의 값이 localStorage에 같이 저장됩니다.
Atom 값 LocalStorage 연동하기
atomWithStorage 설정해보기
/src/atoms/darkMode.ts
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
const darkModeAtom = atomWithStorage("darkMode", false);
//writeonly mode
const darkModeOff = atom(null, (get, set) => {
set(darkModeAtom, false);
});
const darkModeOn = atom(null, (get, set) => {
set(darkModeAtom, true);
});
export { darkModeAtom, darkModeOff, darkModeOn };
- set함수로 darkModeAtom의 값을 변경해보았습니다. atomWithStorage를 쓰면 atom의 값이 기본값으로 된 localStorage에 같이 저장됩니다.
atomWithStorage 사용해보기
/src/components/DarkMode.tsx
import { darkModeOn, darkModeOff, darkModeAtom } from "../atoms/darkModeAtoms";
import { useAtom } from "jotai";
import { useState } from "react";
const DarkMode = () => {
const [mode] = useAtom(darkModeAtom);
const [, setDarkModeOn] = useAtom(darkModeOn);
const [, setDarkModeOff] = useAtom(darkModeOff);
const [storage, setStorage] = useState<string | null>("");
function getLocalStorage() {
return setStorage(localStorage.getItem("darkMode"));
}
return (
<div>
<div>
<h4>darkModeAtom current State : {`${mode}`}</h4>
</div>
<div>
<button onClick={getLocalStorage}>get localStorage</button>
</div>
<div>
<h4>localStorage darkMode state : {`${storage}`}</h4>
</div>
<div>
<button onClick={setDarkModeOn}>Dark mode On</button>
</div>
<div>
<button onClick={setDarkModeOff}>Dark mode Off</button>
</div>
</div>
);
};
export default DarkMode;
- 다크모드가 설정되어 있는지 확인 가능하게 getLocalStorage로 로컬스토리지의 키값에 접근하였습니다.
자세한 코드는 https://github.com/gogleset/react_begin /jotai 디렉토리에 있습니다.