Jotai

2023-08-03
ReactJotaiState-management

Table of Contents

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

jotaimov

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

jotaimov

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 jotaimov

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 jotaimov

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 디렉토리에 있습니다.