Recoil

yarn add recoil @types/recoil

주요 API 호출방법

useRecoilState() - Atom을 읽고 쓸 때 사용한다.

1
2
3
4
5
6
7
8
const counterState = atom({
  key: "counterState",
  default: 0,
});

const [counter, setCounter] = useRecoilState(counterState);

const onIncrement = () => setCounter((prev) => prev + 1); // counter increases

useRecoilValue() - Atom의 값을 반환하고 컴포넌트를 해당 atom에 구독한다.

1
2
3
4
5
6
7
8
const userState = atom({
  key: "userState",
  default: { name: "moon", age: "20" },
});

const user = useRecoilValue(userState);

console.log(user.name, user.age); // moon, 20

useSetRecoilState() - 쓰기 가능한 Atom을 업데이트하기 위한 setter 함수를 반환한다.

1
2
3
4
5
6
7
8
const userState = atom({
  key: "userState",
  default: { name: "moon", age: "20" },
});

const setUserState = userSetRecoilState(userState);

setUserState((prev) => ({ ...prev, name: "ga" })); // name change to "ga"

useResetRecoilState() - Atom을 기본값으로 리셋하고 컴포넌트를 해당 atom에 구독한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const userState = atom({
  key: "userState",
  default: { name: "moon", age: "20" },
});

const setUserState = userSetRecoilState(userState);
const resetUserState = useResetRecoilState(userState);

setUserState((prev) => ({ ...prev, name: "ga" })); // name changed to "ga"
resetUserState(userState); // name reset to "moon"

useRecoilStateLoadable() - 비동기 selector로부터 상태를 불러올 때, 상태가 불러와질 때까지 대기하고 setter가 포함된 Loadable 객체를 반환한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const [userState, setUserState] = useRecoilStateLoadable(
  fetchUserState(userID)
);
switch (userState.state) {
  case "hasValue":
    return <div>{userState.contents.name}</div>;
  case "loading":
    return <div>Loading...</div>;
  case "hasError":
    throw userState.contents;
}

useRecoilValueLoadable() - useRecoilStateLoadable()과 유사하지만 setter가 포함되지 않은 Loadable 객체를 반환한다.

1
2
3
4
5
6
7
8
9
const userState = useRecoilValueLoadable(fetchUserState(userID));
switch (userState.state) {
  case "hasValue":
    return <div>{userState.contents.name}</div>;
  case "loading":
    return <div>Loading...</div>;
  case "hasError":
    throw userState.contents;
}

기본적인 atom, selector 사용 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export const meState = atom<string>({
  key: "me",
  default: "채수현",
});

export const meStateSelector = selector<string>({
  key: "testSelector",
  get: ({ get }) => {
    const me = get(meState);
    // const newMe = DO SOMETHING...
    return newMe;
  },
  set: ({ set }, newValue) => {
    set(meState, newValue);
  },
});

비동기 처리방법 (⭐️공식문서 꼭 보기)

atom, selector, suspense를 이용
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export const meState = atom<string>({
  key: 'meState',
  default: '홍길동',
});

export const fetchMeState = selector<any>({
  key: 'fetchMeState',
  get: async () => {
    try {
      const newMe = await getMe(); // {name: '채수현'}
      return newMe.name;
    } catch (err) {
      throw Error('err');
    }
  },
});
...
export function Test() {
  const [me, setMe] = useRecoilState(meState);
  const fetchMe = useRecoilValue(fetchMeState);

  useEffect(() => {
    setMe(fetchMe);
  }, [fetchMe, setMe]);

  return (
      <div>{me}</div>
  );
}
...
// App.tsx
<Suspense fallback={<div>loading</div>}>
    <PageLayout>
        <Routes />
    </PageLayout>
</Suspense>
useRecoilValueloadable 로 Suspense 없이 사용
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export const fetchMeState = selector<string>({
  key: "fetchMeState",
  get: async () => {
    try {
      const newMe = await getMe(); // {name: '채수현'}
      return newMe.name;
    } catch (err) {
      throw Error("err");
    }
  },
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 이렇게 사용하거나
export function test() {
  const fetchMe = useRecoilValueLoadable(fetchMeState);
  return (
    <>
      {fetchMe.state === "hasValue" ? (
        <div>{fetchMe.contents}</div>
      ) : (
        <div>Loading...</div>
      )}
    </>
  );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 이렇게 사용할 수 있음
export function test() {
  const [me, setMe] = useRecoilState(meState);
  const fetchMe = useRecoilValueLoadable(fetchMeState);
  useEffect(() => {
    if (fetchMe.state === "hasValue") {
      setMe(fetchMe.contents);
    }
  }, [fetchMe.contents, fetchMe.state, setMe]);
  return <div>{me}</div>;
}
useCallback, useEffect로 fecth하고 저장
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const [myName, setMyname] = useRecoilState(myNameState);
const fetch = useCallback(async () => {
  try {
    const { name } = await getMe();
    setMyname(name);
  } catch (err) {
    console.error(err);
  }
}, [setMyname]);

useEffect(() => {
  fetch();
}, [fetch]);
interval + 비동기가 필요할 때
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const [myName, setMyname] = useRecoilState(myNameState);
useEffect(() => {
  const interval = setInterval(async () => {
    if (myName) {
      const { progress } = await getUserInfo(myName); // 'progressing' | 'error' | 'complete'
      if (progress === "complete") {
        // const newName = Do Something
        // setMyname(newName)
      }
    }
  }, 1000);
  return () => clearInterval(interval);
}, [myName, router]);

Library

yarn add js-cookie @types/js-cookie

1
2
3
4
5
6
Cookies.set("name", "value");
Cookies.get("name"); // => 'value'
Cookies.get("nothing"); // => undefined
Cookies.remove("name");
Cookies.set("name", "value", { domain: "subdomain.site.com" });
Cookies.get("name"); // => undefined (need to read at 'subdomain.site.com')

Custom Hook

useAsync (blog)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { AxiosError, AxiosRequestConfig } from "axios";
import { useCallback, useReducer } from "react";

type StateType<T = any> = {
  data: T | null;
  loading: boolean;
  error: AxiosError | null;
};

type ActionType<T> = {
  type: string;
  data?: T;
  error?: AxiosError;
};

type Reducer<T = any> = (
  state: StateType<T>,
  action: ActionType<T>
) => StateType<T>;

const reducer: Reducer = (state, action) => {
  switch (action.type) {
    case "LOADING":
      return {
        data: null,
        loading: true,
        error: null,
      };
    case "SUCCESS":
      return {
        data: action.data as any,
        loading: false,
        error: null,
      };
    case "ERROR":
      return {
        data: null,
        loading: false,
        error: action.error as AxiosError,
      };
    default:
      return state;
  }
};

export type AsyncFc<TResult> = (
  [...arg]: any[],
  config: AxiosRequestConfig
) => Promise<TResult>;

const useAsync = <TResult>(
  callback: AsyncFc<TResult>,
  config: AxiosRequestConfig = {}
) => {
  const [state, dispatch] = useReducer<Reducer<TResult>>(reducer, {
    data: null,
    loading: false,
    error: null,
  });

  const run = useCallback(
    async (...args) => {
      dispatch({ type: "LOADING" });

      try {
        const data = await callback([...args], config);
        dispatch({ type: "SUCCESS", data });

        return data;
      } catch (error) {
        dispatch({ type: "ERROR", error });
      }
    },
    [callback, config]
  );

  return { ...state, run };
};

export default useAsync;
Callback 인자로 들어오는 api 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { AsyncFc } from "~shared/hooks/useAsync";

export const patchRenewal: AsyncFc<RenewalItem> = async (
  [id, params],
  config
) => {
  const response = await apiClient.patch(
    `/api/contracts/issues/${id}`,
    params,
    config
  );

  return response.data;
};
컴포넌트 단 사용 예시
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useState } from "react";

import useAsync from "~shared/hooks/useAsync";
import { User } from "~shared/schemas/user";
import { updateUser } from "../../api";

const Users = () => {
  const { run, loading, error } = useAsync<User>(updateUser);
  const [users, setUsers] = useState<User[]>([]);

  const handleClick = async (user: any) => {
    const data = await run(user.id, user);
    if (data) {
      const next = users.map((user) => (user.id === data.id ? data : user));
      setUsers(next);
    }
  };

  if (loading) return "Loading...";
  if (error) return `Something went wrong: ${error.message}`;

  return (
    <ul>
      {users.map((user: any) => (
        <li key={user.id}>
          <button onClick={() => handleClick(user)}>Update User</button>
          {/*{users 의 상태값을 변경하는 코드...}*/}
        </li>
      ))}
    </ul>
  );
};

export default Users;

계속 …