23/12/28 typescript , thunk 사용한 todolist
2023. 12. 29. 08:49ㆍ카테고리 없음
//전반적으로 어제 작성한 react-query버전과 전체적인 틀은 똑같다.
<Nav.tsx>
import React, { useState } from "react";
import styled from "styled-components";
import { useDispatch } from "react-redux"; //thunk도 유즈 디스패치 사용하니까 임포트
import { __addTodo } from "../redux/mo/modules/todoSlice"; //thunk사용할때 필요한 엑스트라 리듀서인 __addTodo
import { nanoid } from "nanoid";
import { AppDispatch } from "../redux/mo/store/configstore"; //앱 디스패치라고 해당 타입을 해당 폴더에서 만든건데
//일단 임포트export default function Nav() {
type T = { id: string; title: string; content: string; isDone: boolean };
const dispatch = useDispatch<AppDispatch>(); //그 임포트한것이 바로 유즈 디스패치의타입
const JSON_SERVER_BASE_URL = "http://localhost:4000/todos"; //제네릭안에 넣어준다.
const initialForm = {
id: "",
title: "",
content: "",
isDone: false,
};
const [formState, setFormState] = useState<T>(initialForm);
const OnchangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormState((prev: T) => ({ ...prev, [name]: value }));
};
const OnSubmitHandler = async (e: React.FormEvent<HTMLFormElement>) => {
const newTodo = {
id: nanoid(),
title: formState.title,
content: formState.content,
isDone: false,
};
e.preventDefault();
setFormState(initialForm);
dispatch(__addTodo(newTodo)); // 디스패치 안에 리듀서 넣어주고 인자넣어주고
};
// const getTodos = async () => {
// const { data } = await axios.get(JSON_SERVER_BASE_URL);
// const { id, title, content, isDone } = data;
// console.log("첫 데이터", data);
// dispatch(setTodos(data));
// };
return (
<Header onSubmit={OnSubmitHandler}>
<span>제목</span>{" "}
<input
name="title"
value={formState.title}
onChange={OnchangeHandler}
></input>
<span>내용</span>{" "}
<input
name="content"
value={formState.content}
onChange={OnchangeHandler}
></input>
<br />
<button disabled={!formState.title || !formState.content}> //인풋에 정보없으면 작불가
추가하기
</button>
</Header>
);
}
const Header = styled.form`
background-color: lightcyan;
height: 150px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
<TodosList.tsx>
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; //thunk도 RTK이기때문에 스테이트 변경을 위한
import styled from "styled-components"; //디스패치와 스테이트를 읽어오긴위한 유즈샐렉터
import {
__deleteTodo,
__editTodo, //엑스트라 리듀서들
__getTodos,
} from "../redux/mo/modules/todoSlice";
import { AppDispatch } from "../redux/mo/store/configstore"; //역시나 유즈디스패치의 타입을 위해 임포트
export default function TodosList({ listType }: { listType: boolean }) {
type T = { id: string; title: string; content: string; isDone: boolean };
const { todos, isLoading } = useSelector((state: any) => state.todos); //스테이트를 애니라고 했다 귀찮아서 ㅋ
const dispatch = useDispatch<AppDispatch>(); //나중에 깨달은 건데 스테이트는
const JSON_SERVER_BASE_URL = "http://localhost:4000/todos"; //각각의 리듀서를 품고있는 객체형인거 같다.
// const getTodos = async () => {
// const { data } = await axios.get(JSON_SERVER_BASE_URL);
// const { id, title, content, isDone } = data;
// console.log("첫 데이터", data);
// dispatch(setTodos(data));
// };
useEffect(() => {
dispatch(__getTodos()); //리액트 쿼리는 유즈이펙트 필요없지만 첫랜더링시 정보를 가져오기위한
}, [dispatch]); //사용 안에는 정보를 가져올 함수
const DeleteHandler = (id: string) => {
const confirmedData = window.confirm("정말로 삭제할꺼에여?");
if (confirmedData) {
dispatch(__deleteTodo(id)); //삭제로직 인자 id 전달
}
};
const UpdateHandler = (id: string, isDone: boolean) => {
dispatch(__editTodo({ id, isDone })); //수정로직 인자 isDone , id 전달
// updateTodos1(id, isDone);
// getTodos();
};
const notYetTodos = todos.filter((todo: T) => {
return todo.isDone === false;
});
const completedTodos = todos.filter((todo: T) => {
return todo.isDone === true;
});
const changedTodos = listType ? completedTodos : notYetTodos;
if (isLoading) {
return <>로딩중....</>;
}
return (
<>
{" "}
<div style={{ backgroundColor: "black", color: "white" }}>
{listType ? "완료된투두" : "해야할투두"}
</div>
<Section1>
<>
{changedTodos?.map((todo: T) => {
return (
<ListWrapper key={todo.id}>
<div>아이디 : {todo.id}</div>
<div>제목 : {todo.title}</div>
<div>내용 : {todo.content}</div>
<div>상태 : {todo.isDone ? "완료" : "미완료"}</div>
<button onClick={() => UpdateHandler(todo.id, todo.isDone)}>
{listType ? "취소" : "완료"}
</button>
<button onClick={() => DeleteHandler(todo.id)}>삭제하기</button>
</ListWrapper>
);
})}
</>
</Section1>
</>
);
}
const Section1 = styled.section`
height: 200px;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
background-color: lightblue;
`;
const ListWrapper = styled.div`
height: 180px;
width: 180px;
background-color: lightgoldenrodyellow;
`;
<configureStore.ts>
import {
ThunkDispatch,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
import todos from "../modules/todoSlice";
import { useDispatch } from "react-redux";
const rootReducer = combineReducers({}); //이제보니 이건 왜 만들었지? 지워야겠네
const store = configureStore({
reducer: { todos }, //여기까지는 자바스크립트랑 같다.
});
export default store;
export type AppDispatch = typeof store.dispatch; //유즈디스패치를 사용할떄 타입지정요청이 뜨는데
export type RootState = ReturnType<typeof rootReducer>; //요래요래 하면 된다고 해서 해본
export const useAppDispatch: () => AppDispatch = useDispatch; //스토어라는 객체안에 디스패치가 있나봄
// export type AppThunkDispatch = ThunkDispatch<ReducerType, any, Action<string>>;
<todoSlice.ts>
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const getTodos = async () => { //계속사용할 비동기함수라서 뺴놓음
const { data } = await axios.get<T[]>(JSON_SERVER_BASE_URL);
// const { id, title, content, isDone } = data;
console.log("첫 데이터", data);
return data;
};
type T = { id: string; title: string; content: string; isDone: boolean };
const JSON_SERVER_BASE_URL = "http://localhost:4000/todos";
type U = {
todos: T[];
isLoading: boolean; //타입지정
isError: boolean;
error: any;
};
const initialState: U = {
todos: [],
isLoading: true,
isError: false,
error: null,
};
export const __getTodos = createAsyncThunk( //thunk의 사용 방식
"getTodos", //네이밍
async (_, thunkAPI: any) => { //thunkAPI의 타입을 내가 어떻게 아냐구용~
try { // 어디서 알아야되는건지알려줘유
const todos = await getTodos();
return todos; //얘가 액션 함수 였나? 엑스트라 리듀서로 보내주기위한 값 리턴
} catch (err) {
return thunkAPI.rejectWithValue(err); //요게 에러일때 에러값을 리턴해 주는 로직
} //thunk 만든사람이 이렇게 만들었으니 군말없이 쓰도록
}
);
export const __editTodo = createAsyncThunk(
"editTodo",
async ({ id, isDone }: { id: string; isDone: boolean }, thunkAPI) => { //첫번째 인자가 액션함수 쓴곳에서받은 인자
try { //두번쨰가 thunkAPI 임 근데 여기 타입안넣는데 오류 없네?
const { data } = await axios.patch(`${JSON_SERVER_BASE_URL}/${id}`, {
isDone: !isDone, //인자로 받은 아이디에 맞는 애 수정로직
});
const todos = await getTodos(); //자동 스테이트 변경해주는게 아니므로 한번데 data read 함수 호출
return todos;
} catch (err) {
return thunkAPI.rejectWithValue(err);
}
}
);
export const __deleteTodo = createAsyncThunk(
"deleteTodo",
async (id: string, thunkAPI) => {
try {
await axios.delete(`${JSON_SERVER_BASE_URL}/${id}`); //삭제로직 이하동문
const todos = await getTodos();
return todos;
} catch (err) {
return thunkAPI.rejectWithValue(err);
}
}
);
export const __addTodo = createAsyncThunk(
"addTodo",
async (newTodo: T, thunkAPI: any) => { //인자로 뉴투두 받아옴 쟤가 payload임
try {
await axios.post(JSON_SERVER_BASE_URL, newTodo); //새 객체 추가로직
const todos = await getTodos();
return todos;
} catch (err) {
return thunkAPI.rejectWithValue(err);
}
}
);
const todoSlice = createSlice({
name: "todos",
initialState, //이제 투두 슬라이스 만드는 데 thunk는 reducers 키값에 할당하는게 아니라
reducers: {
// setTodos: (state, action) => {
// return action.payload;
// },
//
// addTodos: (state, action) => {
// const newtodo = action.payload;
// return [newtodo, ...state];
// },
// deleteTodo: (state, action) => {
// const id = action.payload;
// const fiteredTodos = state.filter((todo: T) => {
// return todo.id !== id;
// });
// return fiteredTodos;
// },
// updateTodo: (state, action) => {
// const id = action.payload;
// const ChangedTodos = state.map((todo: T) => {
// if (id === todo.id) {
// return { ...todo, isDone: !todo.isDone };
// } else {
// return todo;
// }
// });
// return ChangedTodos;
// },
},
extraReducers: (builder) => { //엑스트라 리듀서에 할당
builder.addCase(__addTodo.pending, (state) => { //pending은 데이트 로딩중
state.isLoading = true;
});
builder.addCase(__addTodo.fulfilled, (state, action) => { //fulfilled는 데이터를 제대로 받았을떄
state.isLoading = false;
state.todos = action.payload;
state.isError = false;
state.error = null;
});
builder.addCase(__addTodo.rejected, (state, action) => { //rejected는 데이터 못받았을떄 쯕 에러일때
state.isLoading = false; //로 각각구분
state.isError = true;
state.error = action.payload;
});
builder.addCase(__getTodos.pending, (state) => { //액션함수 위에서 4개쓰므로 하나당 또 3개
state.isLoading = true; //를 만들어야 하기때문에 builder.addCase가 12개
});
builder.addCase(__getTodos.fulfilled, (state, action) => {
state.isLoading = false;
state.todos = action.payload;
state.isError = false;
state.error = null;
});
builder.addCase(__getTodos.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.error = action.payload;
});
builder.addCase(__deleteTodo.pending, (state) => { //pending때는 딱히 데이터가 없으므로
state.isLoading = true; //걍 isLoading상태가 트루
});
builder.addCase(__deleteTodo.fulfilled, (state, action) => { //데이터를 받았을때가 중요한데
state.isLoading = false;
state.todos = action.payload; //액션함수 4개다 그냥 페이로드가 state.todos이므로
state.isError = false; //다 갈아껴주고 끝
state.error = null;
});
builder.addCase(__deleteTodo.rejected, (state, action) => { //rejected 됐을때는 페이로드로 에러 내용을
state.isLoading = false; //주므로 state.error에 할당
state.isError = true;
state.error = action.payload;
});
builder.addCase(__editTodo.pending, (state) => {
state.isLoading = true;
});
builder.addCase(__editTodo.fulfilled, (state, action) => { //fulfilled 되면 isLoading은 끝났으므로 false
state.isLoading = false;
state.todos = action.payload;
state.isError = false; //에러있니? no = false
state.error = null; //에러내용 당연히 없고
});
builder.addCase(__editTodo.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.error = action.payload;
});
},
});
export default todoSlice.reducer;
// export const { /*addTodos, deleteTodo, updateTodo,*/ setTodos } =
// todoSlice.actions;
<index.ts>
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/mo/store/configstore";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<Provider store={store}> // 스토어 리듀서 툴킷 썽크 등 쓸려면 요거 셋팅 해줘야한다!!
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
<index.ts> 이건 어제 설명 못한 react-query때 의 인덱스.ts
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
// import { Provider } from "react-redux";
// import store from "./redux/mo/store/configstore";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; //두개 임포트
const queryclient = new QueryClient(); //요래요래 해줘야하고
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<QueryClientProvider client={queryclient}> //요래요래 묶어줘야 한다 원리는 모름 ㅋㅋ
{/* <Provider store={store}> */}
<App />
{/* </Provider> */}
</QueryClientProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
//thunk가 확실히 react-query 보다 귀찮은거 같다. ㅋ
//typescript는 확실히 더 연습해서 다음에 next.js랑 같이 프로젝트에 써먹어야 겠다운