WEB/REACT

[REACT] react + typescript + recoil todo list 만들기

자바칩 프라푸치노 2021. 12. 21. 20:17

[Recoil이란?]

 

https://recoiljs.org/ko/docs/introduction/motivation

 

동기 | Recoil

호환성 및 단순함을 이유로 외부의 글로벌 상태관리 라이브러리보다는 React 자체에 내장된 상태 관리 기능을 사용하는 것이 가장 좋다.

recoiljs.org

보통 리액트에서는 전역 상태 관리로 리덕스를 많이 사용했다. 

recoil은 리덕스와 마찬가지로 전역으로 데이터를 구독해서 컴포넌트에서 사용할 수 있는데 훨씬 간단한 라이브러리이다.

 


 

[사용법]

recoil/todo.ts 파일을 만든다.

import { atom } from "recoil";
export interface ITodoTypes {
  id: number;
  contents: string;
  isCompleted: boolean;
}
export const inputState = atom<string>({
  key: "inputState",
  default: "",
});

export const todoState = atom<ITodoTypes[]>({
  key: "todos",

  default: [
    {
      id: 1,
      contents: "스트레칭 하기",
      isCompleted: false,
    },
  ],
});

이렇게 atom으로 지정하면 전역적으로 이 todoState와, inputState에 접근할 수 있게 된다.

key는 고유한 값이다. 다른 atom과 중복되면 안된다.

 

 

todo list를 생성하고 삭제하고 완료 표시를 하려면 

전역에 있는 데이터의 배열에 접근해서 수정할 수 있어야한다.

이것은 컴포넌트에서 리액트 훅 처럼 간편하게 사용할 수 있는데

두가지 방법이 있다.

 

1번

const [todos, setTodos] = useRecoilState<ITodoTypes[]>(todoState);

useRecoilState를 사용하여 마치 useState처럼 사용하는 방법

todos의 initialState는 atom으로 생성한 todoState이고,

setTodos를 사용하면 전역 데이터가 변경된다.

 

2번

const todos = useRecoilValue<ITodoTypes[]>(todoState);
const setTodo = useSetRecoilState<ITodoTypes[]>(todoState);

todos와 setter을 따로 쓰는 법

useRecoilValue를 사용해서 atom으로 만든 todoState를 가져오고,

useSetRecoilState를 사용해서 set을 한다(데이터 변경)

 

 

 

 


[추가하기]

//pages/RecoilPrc.tsx

import React, {
  ChangeEvent,
  KeyboardEventHandler,
  useCallback,
  useState,
} from "react";
import {
  useRecoilState,
  selector,
  useSetRecoilState,
  useRecoilValue,
} from "recoil";
import TodoItem from "../components/TodoItem";
import { numberLabelState, numberState } from "../state/state";
import { Input, Button } from "@mui/material";
import { inputState, ITodoTypes, todoState } from "../recoil/todo";

const RecoilPrc = (): JSX.Element => {
  const [content, setContent] = useRecoilState<string>(inputState);

  const todos = useRecoilValue<ITodoTypes[]>(todoState);
  const setTodo = useSetRecoilState<ITodoTypes[]>(todoState);

  //todo 추가하기
  const addTodo = useCallback((): void => {
    if (!content.trim()) {
      // 빈칸 입력 방지
      return;
    }
    const nextId = todos.length > 0 ? todos[todos.length - 1].id + 1 : 0;
    const todo: ITodoTypes = {
      id: nextId,
      contents: content,
      isCompleted: false,
    };

    setTodo([...todos, todo]);
    setContent("");
  }, [content, setContent, setTodo, todos]);
  //todo input text
  const onChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>): void => {
      setContent(e.target.value);
    },
    [setContent]
  );
  //todo add keybord event
  const onKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>): void => {
      if (e.key === "Enter") {
        addTodo();
      }
    },
    [addTodo]
  );

  return (
    <div>
      <h2>This is recoil</h2>
      <div>
        <Input
          type="text"
          placeholder="todo입력"
          value={content}
          onChange={onChange}
          onKeyDown={onKeyDown}
        />
        <Button variant="contained" onClick={addTodo}>
          등록하기
        </Button>
      </div>
      {todos.map((todo: ITodoTypes) => {
        const { id, contents, isCompleted } = todo;
        return (
          <TodoItem
            key={id}
            id={id}
            content={contents}
            isCompleted={isCompleted}
          />
        );
      })}
    </div>
  );
};

export default RecoilPrc;

여기서는 2번 방법을 사용한다.

addTodo메소드를 보면 useState처럼 사용하면 되는 것을 알 수 있다. 

 

 

 

 


[완료상태 업데이트, 삭제하기]

import { Button } from "@mui/material";
import React, { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { ITodoTypes, todoState } from "../recoil/todo";

interface PropTypes {
  id: number;
  content: string;
  isCompleted: boolean;
}
const TodoItem = (props: PropTypes) => {
  const { id, content, isCompleted } = props;
  const [todos, setTodos] = useRecoilState<ITodoTypes[]>(todoState);

  const handleComplete = useCallback(
    (id: number) => {
      setTodos(
        todos.map((todo: ITodoTypes) => {
          return todo.id === id
            ? {
                ...todo,
                isCompleted: !todo.isCompleted,
              }
            : todo;
        })
      );
    },
    [setTodos, todos]
  );

  const deleteTodo = useCallback(
    (id: number) => {
      setTodos(todos.filter((todo: ITodoTypes) => todo.id !== id));
    },
    [setTodos, todos]
  );
  return (
    <div style={{ display: "flex" }}>
      <h2
        onClick={() => {
          handleComplete(id);
        }}
      >
        {id}. {content} {isCompleted ? "o" : "x"}
      </h2>
      <Button variant="contained" onClick={() => deleteTodo(id)}>
        삭제
      </Button>
    </div>
  );
};

export default TodoItem;

여기서는 1번방법으로 사용한다.

마찬가지로 setTodo함수에 파라미터로 todoState에 들어갈 값을 넣으면 된다.

 

 

 

 


[결과]

 

 

728x90