App.tsx

여기서 핵심은 useContext를 기존처럼 사용하면 null 또는 Todo[] 타입이 지정 되기에 자식에서 해당 Context의 value를 사용하려면 체이닝 (?) 방식을 사용해야하는 번거로움이 생김

→ 그래서 useTodoDispatch라는 useContext의 커스텀 함수를 생성해 null 일때랑 아닐때의 타입을 좁히는 함수를 만들어서 사용하자!

import "./App.css";
import React, {
  useState,
  useRef,
  useEffect,
  useReducer,
  useContext,
} from "react";
import Editor from "./components/Editor";
import { Todo } from "./types";
import TodoItem from "./components/TodoItem";

type Action =
  | {
      type: "CREATE";
      data: {
        id: number;
        content: string;
      };
    }
  | { type: "DELETE"; id: number };

function reducer(state: Todo[], action: Action) {
  switch (action.type) {
    case "CREATE": {
      return [...state, action.data];
    }
    case "DELETE": {
      return state.filter((it) => it.id !== action.id);
    }
  }
}

**export const TodoStateContext = React.createContext<Todo[] | null>(null);
export const TodoDispatchContext = React.createContext<{
  onClickAdd: (text: string) => void;
  onClickDelete: (id: number) => void;
} | null>(null);**

**// context에서 dispatch가 null이거나 아닌 것을 타입 좁혀주는 함수 만들기\\
// 타입 좁혀주기 위해 커스텀으로 만든 useContext
export function useTodoDispatch() {
  const dispatch = useContext(TodoDispatchContext);
  if (!dispatch) throw new Error("TodoDispatchContext에 문제가 있다.");
  return dispatch;
}**

function App() {
  const [todos, dispatch] = useReducer(reducer, []);

  const idRef = useRef(0);

  const onClickAdd = (text: string) => {
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        content: text,
      },
    });
  };

  const onClickDelete = (id: number) => {
    dispatch({
      type: "DELETE",
      id: id,
    });
  };

  useEffect(() => {
    console.log(todos);
  }, [todos]);

  return (
    <div className="App">
      <h1>Todo</h1>
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider
          value={{
            onClickAdd,
            onClickDelete,
          }}
        >
          <Editor>
            <div>child</div>
          </Editor>
          <div>
            {todos.map((todo) => (
              <TodoItem key={todo.id} {...todo} />
            ))}
          </div>
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;

Editor.tsx

import { ReactElement, useContext, useState } from "react";
import { TodoDispatchContext, useTodoDispatch } from "../App";

interface Props {
  children: ReactElement;
}

export default function Editor(props: Props) {
  **const dispatch = useTodoDispatch();**

  // "" 를 초기값으로해서 string 타입으로 자동 추론해줌
  const [text, setText] = useState("");

  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const onClickButton = () => {
    dispatch.onClickAdd(text);
    setText("");
  };
  return (
    <div>
      <input value={text} onChange={onChangeInput} />
      <button onClick={onClickButton}>추가</button>
    </div>
  );
}

TodoItem.tsx

import { useTodoDispatch } from "../App";
import { Todo } from "../types";

// Todo의 타입을 다 가진 Props 타입이 만들어짐
interface Props extends Todo {}

export default function TodoItem(props: Props) {
  **const dispatch = useTodoDispatch();**
  const onClickButton = () => {
    dispatch.onClickDelete(props.id);
  };

  return (
    <div>
      {props.id}번 : {props.content}
      <button onClick={onClickButton}>삭제</button>
    </div>
  );
}