Stack/TypeScript

[TS / React] Redux로 To-do List 구현

7ingout 2022. 7. 29. 15:15

2022.07.28 - [Coding/TypeScript] - [TS / React] Redux로 Counter 구현

 

[TS / React] Redux로 Counter 구현

react-redux 1. 모듈리듀서 1) 액션타입 지정 2) 액션생성 함수 3) 초기값 4) 리듀서 함수 작성 -> 루트리듀서 2. 스토어 생성 const store = createStore(루트리듀서) 3.컴포넌트 생성 1) 컨테이너 컴포넌트 2)..

7ingout.tistory.com

초기 셋팅은 위 게시글 참고 ~

 

TS-REDUX-TUTORIAL

modules/todos.ts

// 액션타입 선언, 액션 생성 함수, 초기값, 리듀서
// 할 일 추가, 할 일 제거, 할 일 체크

// 액션타입 선언
const ADD_TODO = 'todos/ADD_TODO' as const;
const TOGGLE_TODO = 'todos/TOGGLE_TODO' as const;
const REMOVE_TODO = 'todos/REMOVE_TODO' as const;

let nextId = 1;
// 액션 생성 함수
export const addTodo = (text: string) => ({
    type: ADD_TODO,
    payload: {
        id: nextId++,
        text
    }
})
export const toggleTodo = (id:number) => ({
    type: TOGGLE_TODO,
    payload: id
})
export const removeTodo = (id:number) => ({
    type: REMOVE_TODO,
    payload: id
})

// 액션 객체들에 대한 타입 작성
type TodosAction = 
| ReturnType<typeof addTodo>
| ReturnType<typeof toggleTodo>
| ReturnType<typeof removeTodo>

// 상태에서 사용할 할일 항목 데이터 정리
export type Todo = {
    id: number;
    text: string;
    done: boolean;
}

// 이 모듈에서 관리할 상태 타입 작성
export type TodoState = Todo[]

// 초기상태 선언
const initialState: TodoState = [];

// 리듀서 작성하기
function todos(
    state: TodoState = initialState,
    action: TodosAction
) : TodoState {
    switch(action.type) {
        case ADD_TODO:
            return state.concat({
                id:action.payload.id,
                text: action.payload.text,
                done: false
            });
        case TOGGLE_TODO:
            return state.map(todo=>
                todo.id === action.payload ? {...todo, done: !todo.done } : todo
                )
        case REMOVE_TODO:
            return state.filter(todo => todo.id !== action.payload);
        default:
            return state
    }
}
export default todos;

 

modules/index.ts

import { combineReducers } from "redux";
import counter from "./counter"
import todos from "./todos"

const rootReducer = combineReducers({ counter, todos });
export default rootReducer;

// 나중에 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야하므로
// 내보내줍니다.
export type RootState = ReturnType<typeof rootReducer>

 

// 프레젠테이션 컴포넌트

TodoInsert.tsx

import React, { FormEvent, useState } from 'react';

// props로 전달받을 데이터 타입을 지정
type TodoInsertProps = {
    onInsert: (text:string) => void;
}
const TodoInsert = ({onInsert}: TodoInsertProps) => {
    const [value, setValue] = useState('')
    const onChange = (e:React.ChangeEvent<HTMLInputElement>) => {
        setValue(e.target.value)
    }
    const onSubmit = (e: FormEvent) => {
        e.preventDefault();
        onInsert(value);
        setValue("")
    }
    return (
        <div>
            <form onSubmit={onSubmit}>
                <input onChange={onChange} value={value} placeholder="할 일을 입력하세요"/>
                <button type="submit">등록</button>
            </form>
        </div>
    );
};

export default TodoInsert;

 

TodoItem.tsx

import React, { CSSProperties } from 'react';
import { Todo } from '../modules/todos';

// props 타입 지정
type TodoItemProps = {
    todo: Todo;
    onToggle: (id: number) => void;
    onRemove: (id: number)=> void;
}
const TodoItem = ({ todo, onToggle, onRemove }: TodoItemProps) => {
    const handleToggle = () => onToggle(todo.id)
    const handleRemove = () => onRemove(todo.id)
    const textStyle: CSSProperties= {
        textDecoration: todo.done? 'line-through' : 'none'
    }
    const removeStyle: CSSProperties = {
        color: 'red',
        marginLeft: 8
    }
    return (
        <li>
            <span onClick={handleToggle} style={textStyle}>{todo.text}</span>
            <span onClick={handleRemove} style={removeStyle}>X</span>
        </li>
    );
};

export default TodoItem;

 

TodoList.tsx

import React from 'react';
import { Todo } from '../modules/todos';
import TodoItem from './TodoItem';

// props 타입지정
type TodolistProps = {
    todos: Todo[];
    onToggle:(id:number) => void;
    onRemove:(id:number) => void;
}
const TodoList = ({todos, onToggle, onRemove}: TodolistProps) => {
    if(todos.length === 0 ) return <div>등록한 항목이 없습니다.</div>
    return (
        <ul>
            {todos.map(todo=>(
                <TodoItem todo={todo} onToggle={onToggle} onRemove={onRemove} key={todo.id}/>
            ))}
        </ul>
    );
};

export default TodoList;

//

 

containers/TodoApp.tsx

import React from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import TodoInsert from '../components/TodoInsert';
import TodoList from '../components/TodoList';
import { RootState } from '../modules';
import { addTodo, removeTodo, toggleTodo } from '../modules/todos';

const TodoApp = () => {
    const todos = useSelector((state: RootState) => state.todos)
    const dispatch = useDispatch();

    const onInsert = (text: string) => {
        dispatch(addTodo(text))
    }
    const onToggle = (id: number) => {
        dispatch(toggleTodo(id))
    }
    const onRemove = (id: number) => {
        dispatch(removeTodo(id))
    }
    return (
        <div>
            <TodoInsert onInsert={onInsert}/>
            <TodoList todos={todos} onToggle={onToggle} onRemove={onRemove}/>
        </div>
    );
};

export default TodoApp;

 

App.tsx

import React from 'react';
import './App.css';
// import ContainerCounter from './containers/ContainerCounter';
import TodoApp from './containers/TodoApp';

function App() {
  return (
    <div className="App">
        {/* <ContainerCounter /> */}
        <TodoApp />
    </div>
  );
}

export default App;