기존에 Redux, Redux-toolkit, Redux-saga를 이용해서 상태 관리를 했었는데 코드가 너무 길어져서 다른 상태관리법을 찾다가 Recoil 알게됐다. Recoil을 이용해서 간단한 todolist를 만들어보자
npm install recoil
기존에 Redux에서와 비슷하게 감싸지만 안에 따로 store은 넣지 않는다.
//Redux version
<Provider store={store}>
<App />
</Provider>
//Recoil version
<RecoilRoot>
<App />
</RecoilRoot>
interface todoListArrayAtomInterface {
todoValue: string;
id: number;
checked: boolean;
}
export const todoListArrayAtom = atom<todoListArrayAtomInterface[]>({
key: "todoListArrayAtom",
default: [],
});
Atoms는 상태의 단위이며, 업데이트와 구독이 가능하다. atom이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영하여 다시 렌더링 된다. atoms는 런타임에서 생성될 수도 있다. Atoms는 React의 로컬 컴포넌트의 상태 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.(Recoil본문...)
key : 고유의 키값이다. 다른 atom, selector와 같으면 안된다.
default: 상태의 기본 값이다.
export const checkTodoItem = selector({
key: "checkTodoItem",
get: ({ get }) => {
const todoListArr = get(todoListArrayAtom);
const unresolvedTodoLen = todoListArr.filter(
(todoItem) => todoItem.checked === false
).length;
return `남은 할것들 ${unresolvedTodoLen}개 `;
},
});
Selector는 atom 또는 selector의 값을 가져와 가공할 수 있다. atom 또는 selector가 업데이트되면 하위의 selector 함수도 다시 실행된다. 컴포넌트들은 selector를 atom처럼 구독할 수 있으며 selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
key : 고유의 키값이다. 다른 atom, selector와 같으면 안된다.
get : get으로 다른 atom, selector를 가져올 수 있다. return값으로 useRecoilValue에서 읽을 수 있는 값을 내보낸다.
set ?: set을 사용하면 쓰기 가능한 상태로 바뀐다. 다른 atom, selector의 상태를 변화시킬 수 있다.
recoil사이트 를 들어가보면 "Recoil은 React처럼 작동하고 생각합니다."라고 적혀 있다. useRecoilState, useRecoilValue, useSetRecoilState들이 리액트의 useState와 매우 유사하게 생겼고 사용법도 유사하다.
const [todoListArray, setTodoListArray] = useRecoilState(todoListArrayAtom);
useState처럼 배열을 반환하고 첫번째 값으로 상태값, 두번째 값으로 상태의 값을 바꿀 수 있는 함수가 나온다.
const todoListArray = useRecoilValue(todoListArrayAtom);
useRecoilState에서 return되는 배열중에 첫번째 값만 가져온다. 읽기전용일때 사용된다.
const setTodoListArray = useSetRecoilState(todoListArrayAtom);
useRecoilState에서 return되는 배열중에 두번째 값만 가져온다.
import { atom, selector } from "recoil";
interface todoListArrayAtomInterface {
todoValue: string;
id: number;
checked: boolean;
}
export const todoListArrayAtom = atom<todoListArrayAtomInterface[] | []>({
key: "todoListArrayAtom",
default: [],
});
export const checkTodoItem = selector({
key: "checkTodoItem",
get: ({ get }) => {
const todoListArr = get(todoListArrayAtom);
const unresolvedTodoLen = todoListArr.filter(
(todoItem) => todoItem.checked === false
).length;
return `남은 할것들 ${unresolvedTodoLen}개 `;
},
});
우선 todoListArrayAtom라는 to do list의 내용들을 배열 형태로 저장시키는 atom을 만든다. 그리고 checkTodoItem로 남은 todo가 몇개인지 알려주는 selector를 하나 만들어준다.
import { useState, ChangeEvent, FormEvent } from "react";
import { useSetRecoilState } from "recoil";
import { todoListArrayAtom } from "./store";
export function InputAndButton() {
const [inputValue, setInputValue] = useState("");
const setTodoListArray = useSetRecoilState(todoListArrayAtom);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleOnSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setTodoListArray((prevTodoListArray) => [
...prevTodoListArray,
{ todoValue: inputValue, id: getId(), checked: false },
]);
setInputValue("");
};
return (
<form onSubmit={handleOnSubmit}>
<input value={inputValue} onChange={handleOnChange} />
<button>추가</button>
</form>
);
}
let todoItemid = 0;
const getId = () => {
return todoItemid++;
};
import { MouseEvent } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import styled from "styled-components";
import { checkTodoItem } from "./store";
import { todoListArrayAtom } from "./store";
export function TodoListDisPlay() {
const [todoListArray, setTodoListArray] = useRecoilState(todoListArrayAtom);
const checkTodo = useRecoilValue(checkTodoItem);
const handleDeleteOnClick = (e: MouseEvent<HTMLButtonElement>) => {
const target = e.target;
if (target instanceof Element) {
setTodoListArray((todoListArr) =>
todoListArr.filter((todoItem) => todoItem.id !== parseInt(target.id))
);
}
};
const handleCheckOnClick = (e: MouseEvent<HTMLButtonElement>) => {
const target = e.target;
if (target instanceof Element) {
setTodoListArray((todoListArr) =>
todoListArr.map((todoItem) =>
todoItem.id !== parseInt(target.id)
? todoItem
: { ...todoItem, checked: !todoItem.checked }
)
);
}
};
return (
<>
<div>{checkTodo}</div>
{todoListArray.map((todoListItem) => (
<div key={todoListItem.id}>
{todoListItem.checked ? (
<CheckedSpan
onClick={handleCheckOnClick}
id={String(todoListItem.id)}
>
{todoListItem.todoValue}
</CheckedSpan>
) : (
<span onClick={handleCheckOnClick} id={String(todoListItem.id)}>
{todoListItem.todoValue}
</span>
)}
<button id={String(todoListItem.id)} onClick={handleDeleteOnClick}>
삭제
</button>
</div>
))}
</>
);
}
const CheckedSpan = styled.span`
text-decoration: line-through;
`;
결과
(잘못된 정보나 피드백 해주실거 알려주시면 바로 반영해서 수정하겠습니다. 읽어주셔서 감사합니다)