https://recoiljs.org/ko/docs/basic-tutorial/intro
리코일 공식 문서를 그대로 따라한 코드이다.
배울만한 점을 복기해본다.
1. TodoList
가장 최상단의 컴포넌트이다.
또한 List+Filter Atom을 조합해 파생한 filteredTodoListState를 화면에 보여준다.
즉 사용자가 보는 데이터는 filtered된 리스트 데이터지만, 해당 컴포넌트는 필터 이전의 List를 갖고 있다.
또한 todoListState 혹은 todoListFilterState가 변경되면 해당 컴포넌트는 리렌더링 된다.
export interface Todo {
id: number;
text: string;
isComplete: boolean;
}
export const todoListState = atom<Todo[]>({
key: "todoListState",
default: []
});
const filteredTodoListState = selector({
key: "FilteredTodoList",
get: ({ get }) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case "Show Completed":
return list.filter((item) => item.isComplete);
case "Show Uncompleted":
return list.filter((item) => !item.isComplete);
default:
return list;
}
}
});
export default function TodoList() {
// 동시에 읽고 쓰기 위함.
const todoList = useRecoilValue(filteredTodoListState);
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
2. TodoListStats
TodoList 컴포넌트에 있던 todoListState를 이용해 통계 데이터를 조합하여 화면에 뿌려준다.
해당 컴포넌트는 todoListState에 의존하여 해당 데이터가 변경되면 자동으로 갱신된다.
export const todoListStatsState = selector({
key: "TodoListStats",
get: ({ get }) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
const totalUncompletedNum = totalNum - totalCompletedNum;
const percentCompleted =
totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted
};
}
});
/**
* Todo list statistics: derived from the complete todo list
* by calculating useful attributes of the list,
* such as the total number of items in the list,
* the number of completed items, and the percentage of items that are completed.
*/
export default function TodoListStats() {
const {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted
} = useRecoilValue(todoListStatsState);
const formattedPercentCompleted = Math.round(percentCompleted);
return (
<ul>
<li>Total items: {totalNum}</li>
<li>Items completed: {totalCompletedNum}</li>
<li>Items not completed: {totalUncompletedNum}</li>
<li>Percent completed: {formattedPercentCompleted}</li>
</ul>
);
}
3. TodoListFilter
필터를 변경하면, 해당 필터를 구독하는 1.TodoList는 filteredTodoListState에 의해 리렌더링된다.
type FilterState = "Show Completed" | "Show All" | "Show Uncompleted";
export const todoListFilterState = atom<FilterState>({
key: "todoListFilterState",
default: "Show All"
});
export default function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoListFilterState);
const updateFilter: React.ChangeEventHandler<HTMLSelectElement> = ({
target: { value }
}) => {
// upcasting always typesafe
setFilter(value as FilterState);
};
return (
<>
Filter:
<select value={filter} onChange={updateFilter}>
<option value="Show All">All</option>
<option value="Show Completed">Completed</option>
<option value="Show Uncompleted">Uncompleted</option>
</select>
</>
);
}
4. TodoItemCreator
해당 컴포넌트는 todoListState 아톰을 useSetRecoilState 만을 이용하여 접근한다.
이는 todoListState가 변경되어도 해당 컴포넌트가 리렌더링 되지 않음을 의미한다.
(React.memo 등은 고려해야 한다)
// 고유한 Id 생성을 위한 유틸리티
let id = 0;
function getId() {
return id++;
}
export default function TodoItemCreator() {
const [inputValue, setInputValue] = useState("");
// useSetRecoilState()을 사용하는 것은 컴포넌트가 값이 바뀔 때 리렌더링을 하기 위해 컴포넌트를 구독하지 않고도 값을 설정하게 해줍니다.
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
// 기존 todo 리스트를 기반으로 새 todo 리스트를 만들 수 있도록 setter 함수의 updater 형식을 사용한다는 점에 유의해야 한다.
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false
}
]);
setInputValue("");
};
const onChange: React.ChangeEventHandler<HTMLInputElement> = ({
target: { value }
}) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
}
5. TodoItem
todoListState의 Update, Read, Delete 책임을 갖고 있는 컴포넌트다.
const replaceItemAtIndex = (
arr: Todo[],
index: number,
newValue: Todo
): Todo[] => {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
};
const removeItemAtIndex = (arr: Todo[], index: number): Todo[] => {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
};
export default function TodoItem({ item }: { item: Todo }) {
const [todoList, setTodoList] = useRecoilState(todoListState);
const index = todoList.findIndex((listItem) => listItem === item);
const editItemText: React.ChangeEventHandler<HTMLInputElement> = ({
target: { value }
}) => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
text: value
});
setTodoList(newList);
};
const toggleItemCompletion = () => {
const newList = replaceItemAtIndex(todoList, index, {
...item,
isComplete: !item.isComplete
});
setTodoList(newList);
};
const deleteItem = () => {
const newList = removeItemAtIndex(todoList, index);
setTodoList(newList);
};
return (
<div>
<input type="text" value={item.text} onChange={editItemText} />
<input
type="checkbox"
checked={item.isComplete}
onChange={toggleItemCompletion}
/>
<button onClick={deleteItem}>X</button>
</div>
);
}
요약 :
Read 갱신이 필요 없는 컴포넌트의 경우 useSetRecoilState 훅을 사용한다.
작은 Atom를 조합하여 큰 Selector를 만든다.
'FrontEnd' 카테고리의 다른 글
Lean Recoil 강의 정리 : Atom Families / Selector Families (0) | 2022.05.15 |
---|---|
Lean Recoil 강의 정리 : Atoms And Selectors (0) | 2022.05.15 |
Recoil 배경 및 기본 알아보기 (0) | 2022.05.14 |
XState 상태 기계로 SWR 구현하기 (0) | 2022.05.05 |
제어의 역전을 활용해 타입 친화적인 컨텍스트를 디자인하는 방법 (0) | 2022.05.05 |