Recoil은 React용 상태 관리 라이브러리이다.
Recoil은 Atom이라는 것을 사용하여 각 구성 요소의 상태를 관리하며, useState와 똑같은 API를 사용하기 때문에 생성하고 사용하기에 매우 쉽다.
recoil 영상 속 이미지
개인적으로 제일 이해하기 편했던 그림들인데 리액트는 아래와 같이 몇 가지 제한 사항이 있다.
- 구성 요소의 상태는 공통 조상으로 푸시해야만 공유할 수 있고 여기에는 다시 렌더링 해야하는 거대한 나무가 포함될 수 있습니다.
- 컨텍스트는 각각 고유한 소비자가 있는 무한한 값 집합이 아니라 단일 값만 저장할 수 있습니다.
- 이 두 가지 모두 트리의 잎(상태가 사용되는 위치)에서 트리의 상단(상태가 있어야 하는 위치)을 코드 분할하기 어렵게 만듭니다.
출처
앱에서 어떤 하나의 상태 변경이 다른 여러 컴포넌트에 영향을 줄 수 있는 상황이라면 그 상태 변경 사항이 다른 컴포넌트에서도 필요하게 되는데 위의 제한사항이 불편함으로 다가온다.
그래서 상태관리 라이브러리를 사용하는데 여러 상태관리 라이브러리가 있지만 Recoil은 API와 의미 체계 및 동작 방식을 가능한 한 React스럽게(Reactish) 가져가면서 현상을 해결하려고 제작했다고 한다.
npm i recoil
예)
//index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { RecoilRoot } from "recoil";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot> //***
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
//atoms.ts
import {atom} from "recoil";
export const counterAtom = atom({
key: "counter",
default : 0
})
atom 기능으로 생성한다. (key는 unique해야함)
default 값은 number, string, object, array, function 등 다양하게 가능하다.
//ComponentIncrement.js
import { counterAtom } from "./atoms";
import { useRecoilState } from "recoil";
export default function ComponentIncrement() {
const [counter, setCounter] = useRecoilState(counterAtom);
const onIncrementClick = () => setCounter(counter + 1);
return (
<div>
<p>Component</p>
<p>{counter}</p>
<input onClick={onIncrementClick} type="submit" value="Increment" />
</div>
)
}
useRecoilState은 useState와 비슷하다고 볼 수 있다.
counter로 counterAtom의 값을 볼 수 있고 setCounter로 counterAtom의 값을 변경할 수 있다.
input을 클릭할 때 마다 onIncrementClick 함수가 실행되어 상태값을 +1 씩 하여 변경한다.
//ComponentValue.js
import { useRecoilValue } from "recoil";
import { counterAtom } from "./atoms";
export default function ComponentValue() {
const counter = useRecoilValue(counterAtom);
return (
<div>
<p>Component Value Only</p>
<p>{counter}</p>
</div>
)
}
useRecoilValue는 상태값을 불러온다.
//App.js
import ComponentIncrement from "./ComponentIncrement";
import ComponentValue from "./ComponentValue";
export default function App() {
return (
<div>
<ComponentIncrement />
<ComponentValue />
</div>
);
}
ComponentIncrement, ComponentValue 각각 나눠진 컴포넌트지만 ComponentIncrement의 input 버튼에 따라 counterAtom 값을 공유하는것을 볼 수 있다.
- useRecoilValue(): atom의 내용을 읽을 수 있다.
- useSetRecoilState(): atom의 내용을 변경할 수 있다.
- useRecoilState(): useRecoilValue & useSetRecoilState 같이 사용 가능하다.
selectors는 상태를 기반으로 하는 파생 데이터를 계산하는데 사용된다. 이것은 최소한의 상태 집합이 atom에 저장되고 다른 모든 것은 해당 최소 상태의 함수로 효율적으로 계산되기 때문에 중복 상태를 피할 수 있다.
어떤 구성 요소가 필요하고 어떤 상태에 의존하는지 추적하기 때문에 이 기능적 접근 방식을 매우 효율적으로 만든다.
//atoms.ts
import { atom, selector } from "recoil";
export enum Categories {
"TO_DO" = "TO_DO",
"DOING" = "DOING",
"DONE" = "DONE",
}
export interface IToDo {
text: string;
id: number;
category: Categories;
}
export const categoryState = atom<Categories>({
key: "category",
default: Categories.TO_DO,
});
export const toDoState = atom<IToDo[]>({
key: "toDo",
default: [],
});
export const toDoSelector = selector({
key: "toDoSelector",
get: ({ get }) => {
const toDos = get(toDoState);
const category = get(categoryState);
return toDos.filter((toDo) => toDo.category === category);
},
});
toDoSelector에서 toDos와 cateforyState를 불러와서 현재 카테고리에 해당하는 toDo 값만 return 한다.
//CreateToDo.tsx
import { useForm } from "react-hook-form";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { categoryState, toDoState } from "./atoms";
interface IForm {
toDo: string;
}
export default function CreateToDo() {
const setToDos = useSetRecoilState(toDoState);
const category = useRecoilValue(categoryState);
const { register, handleSubmit, setValue } = useForm<IForm>();
const handleValid = ({ toDo }: IForm) => {
setToDos((oldToDos) => [
{ text: toDo, id: Date.now(), category },
...oldToDos,
]);
setValue("toDo", "");
};
return (
<form onSubmit={handleSubmit(handleValid)}>
<input
{...register("toDo", {
required: "Please write a To Do",
})}
placeholder="Write a to do"
/>
<button>Add</button>
</form>
);
}
ToDo 항목을 생성하는 컴포넌트로 form의 submit시에 handleValid에서 setToDos를 실행하여 toDo를 현재 카테고리 위치에 추가한다.
//ToDoList.tsx
import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { Categories, categoryState, toDoSelector } from "../atoms";
import CreateToDo from "./CreateToDo";
import ToDo from "./ToDo";
export default function ToDoList() {
const toDos = useRecoilValue(toDoSelector);
const [category, setCategory] = useRecoilState(categoryState);
const onInput = (event: React.FormEvent<HTMLSelectElement>) => {
setCategory(event.currentTarget.value as Categories);
};
return (
<div>
<h1>To Dos</h1>
<hr />
<select value={category} onInput={onInput}>
<option value={Categories.TO_DO}>To Do</option>
<option value={Categories.DOING}>Doing</option>
<option value={Categories.DONE}>Done</option>
</select>
<CreateToDo />
{toDos?.map((toDo) => (
<ToDo key={toDo.id} {...toDo} />
))}
</div>
);
}
ToDo list를 표여주는 컴포넌트이다.
useRecoilValue에 selector 함수를 넣어서 해당하는 조건의 toDos를 받는다.
category로 현재 categoryState의 상태를 확인하고 select의 option을 선택할때마다 setCategory()를 실행하여 categoryState의 상태를 바꿔준다.
그리고 toDos 안의 내용물을 순차적으로 ToDo 컴포넌트로 보내준다.
//ToDo.tsx
import React from "react";
import { useSetRecoilState } from "recoil";
import { Categories, IToDo, toDoState } from "./atoms";
export default function ToDo({ text, category, id }: IToDo) {
const setToDos = useSetRecoilState(toDoState);
const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const {
currentTarget: { name },
} = event;
setToDos((oldToDos) => {
const targetIndex = oldToDos.findIndex((toDo) => toDo.id === id);
const newToDo = { text, id, category: name as IToDo["category"] };
return [
...oldToDos.slice(0, targetIndex),
newToDo,
...oldToDos.slice(targetIndex + 1),
];
});
};
return (
<li>
<span>{text}</span>
{category !== Categories.DOING && (
<button name={Categories.DOING} onClick={onClick}>
Doing
</button>
)}
{category !== Categories.TO_DO && (
<button name={Categories.TO_DO} onClick={onClick}>
To Do
</button>
)}
{category !== Categories.DONE && (
<button name={Categories.DONE} onClick={onClick}>
Done
</button>
)}
</li>
);
}
각 ToDo 를 표시하는 컴포넌트로 카테고리를 변경할 수 있다.
해당 카테고리 버튼을 클릭하면 onClick안의 setToDos를 실행하여 ToDo의 카테고리를 수정한다.
setToDos를 위와같이 안에 함수를 넣어 사용하면 oldToDos안에는 현재 ToDo 전체를 불러온다.
targetIndex는 findIndex로 현재 ToDo의 위치를 찾는다.
newToDo는 현재 ToDo 값을 카테고리만 변경하여 그대로 생성한다.
[...oldToDos.slice(0, targetIndex),
newToDo,
...oldToDos.slice(targetIndex + 1),]
현재 ToDo를 잘라버리고 카테고리를 변경한 새로운 ToDo를 넣어 반환하는 코드이다.
category !== Categories.DOING &&
는 참일경우 &&뒤의 코드를 사용한다.
//App.tsx
import ToDoList from "./ToDoList";
export default function App() {
return (
<ToDoList />
);
}
Dynamic Dependencies
읽기 전용 선택기에는 get종속성을 기반으로 선택기의 값을 평가 하는 메서드가 있습니다. 이러한 종속성이 업데이트되면 선택기가 다시 평가됩니다. 종속성은 선택기를 평가할 때 실제로 사용하는 원자 또는 선택기를 기반으로 동적으로 결정됩니다. 이전 종속성의 값에 따라 다른 추가 종속성을 동적으로 사용할 수 있습니다. Recoil은 현재 데이터 흐름 그래프를 자동으로 업데이트하여 선택자가 현재 종속성 집합의 업데이트에만 가입하도록 합니다.
출처
위의 예제는 selector의 get만 사용하였는데 set도 사용이 가능하다.
Atoms의 예제를 약간만 바꿔봤다.
//atoms.js
import {atom, selector} from "recoil";
export const counterAtom = atom({
key: "counter",
default : 0
})
export const countChange = selector({
key: "selector",
get: ({get}) => get(counterAtom) % 2 === 0,
set: ({set}, newValue) => set(counterAtom, newValue as number)
});
//ComponentValue.tsx
import { useRecoilState, useRecoilValue } from "recoil";
import { counterAtom, countChange } from "./atoms";
export default function ComponentValue() {
const counter = useRecoilValue(counterAtom);
const [value, setCounter] = useRecoilState(countChange);
const onZero = () => setCounter(123);
return (
<div>
<p>Component Value Only</p>
<p>{counter}</p>
<input onClick={onZero} type="submit" value="zero" />
<p>{value ? "짝수" : "홀수"}</p>
</div>
)
}
const [value, setCounter] = useRecoilState(countChange)
일반적인 상태를 확인하고 수정하는것과 생긴것은 동일하다.
위의 예제의 경우 counterAtom의 상태를 직접 변경하는 것과 countChange로 상태를 변경하는 것에 차이는 없지만 set이 어떻게 작동하는지만 확인해보자.
countChange의 get에 해당하는 value를 불러올때 counterAtom의 값이 짝수면 true, 홀수면 false를 가져온다.
set은 첫번째 인자로 객체, 두번째 인자로 새로운 값이 넘어오고, 첫번째 인자 객체의 set을 사용하여 state값을 바꾼다. 이때 첫번째 인자는 recoil state여야하고 두번째 인자는 바뀔 값에 해당한다.
Writeable Selectors
양방향 선택기는 들어오는 값을 매개변수로 수신하고 이를 사용하여 데이터 흐름 그래프를 따라 업스트림으로 변경 사항을 전파할 수 있습니다. 사용자는 선택기를 새 값으로 설정하거나 선택기를 재설정할 수 있기 때문에 들어오는 값은 선택기가 나타내는 것과 동일한 유형이거나 DefaultValue재설정 작업을 나타내는 개체입니다.
이 간단한 선택기는 기본적으로 추가 필드를 추가하기 위해 원자를 래핑합니다. 설정 및 재설정 작업을 통해 업스트림 원자로 전달됩니다.출처
get함수 만 제공되는 경우 선택기는 읽기 전용이며 RecoilValueReadOnly개체를 반환 합니다. set또한 제공되는 경우 쓰기 가능한 RecoilState개체를 반환 합니다.출처
https://nomadcoders.co/react-masterclass
https://recoiljs.org/docs/introduction/getting-started
https://levelup.gitconnected.com/a-new-state-management-for-react-recoil-53ad7480faa4
https://medium.com/humanscape-tech/recoil-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-285b29135d8e
https://ui.toast.com/weekly-pick/ko_20200616
https://kkangil.github.io/2020/05/24/React-recoiljs-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0