여기서 핵심은 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;
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>
);
}
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>
);
}