I'm a beginner learning ts for the first time. Thank you in advance for sharing your knowledge. I am making a to-do list. I used to react to complete it. But now I am using react and typescript together to complete the code.
It seems to me that 'reducer' is not working properly. How can I operate this?
toDos
, completed
Both have errors. My computer is not bringing these things at all.
I'd appreciate it if you let me know. This is 'App.tsx' code with a surface error.
import React from "react";
import Add from "./Add";
import List from "./List";
import ToDo from "./ToDo";
import Title from "./Title";
import Progress from "./Progress";
import styled from "styled-components";
import { useTodosState } from '../context';
function App() {
const { toDos, completed } = useTodosState();
return (
<Title>
<Add />
<Progress />
<Lists>
<List title={toDos.length !== 0 ? "To Dos" : ""}>
{toDos.map((toDo: any) => (
<ToDo key={toDo.id} id={toDo.id} text={toDo.text} isCompleted={false} />
))}
</List>
<List title={completed.length !== 0 ? "Completed" : ""}>
{completed.map((toDo: any) => (
<ToDo key={toDo.id} id={toDo.id} text
{...toDo.text} isCompleted />
))}
</List>
</Lists>
</Title>
);
}
export default App;
This code is the 'reducer.tsx' code that I thought there was a problem.
import { v4 as uuidv4 } from "uuid";
import { ADD, DEL, COMPLETE, UNCOMPLETE, EDIT } from "./actions";
export const initialState = {
toDos: [],
completed: [],
};
interface IReducer {
state: any;
action: any;
}
const Reducer = ({ state, action }: IReducer) => {
switch (action) {
case ADD:
return {
...state,
toDos: [...state.toDos, { text: action.payload, id: uuidv4() }],
};
case DEL:
return {
...state,
toDos: state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.payload),
};
case COMPLETE:
const target = state.toDos.find((toDo: { id: number; }) => toDo.id === action.payload);
return {
...state,
toDos: state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.payload),
completed: [...state.completed, { ...target }],
};
case UNCOMPLETE:
const aTarget = state.completed.find(
(toDo: { id: number; }) => toDo.id === action.payload
);
return {
...state,
toDos: [...state.toDos, { ...aTarget }],
completed: state.completed.filter(
(complete: { id: number; }) => complete.id !== action.payload
),
};
case EDIT:
const bTarget = state.toDos.find((toDo: { id: number; }) => toDo.id === action.id);
const rest = state.toDos.filter((toDo: { id: number; }) => toDo.id !== action.id);
return {
...state,
toDos: rest.concat({ ...bTarget, text: action.payload }),
};
default:
return;
}
};
export default Reducer;
This code is 'context.tsx' code.
import React, { createContext, useReducer, useContext } from 'react';
import Reducer, { initialState } from "./reducer";
export type Todo = {
id: number;
text: string;
done: boolean;
};
export type TodosState = Todo[];
const ToDosContext = createContext<Array<Todo> | any>(null);
const ToDosProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<ToDosContext.Provider value={{ state, dispatch }}>
{children}
</ToDosContext.Provider>
);
};
export const useTodosDispatch = () => {
const { dispatch } = useContext(ToDosContext);
return dispatch;
};
export const useTodosState = () => {
const { state } = useContext(ToDosContext);
return state;
};
export default ToDosProvider;
interface IReducer {
state: any;
action: any;
}
This type is not particularly helpful since your state
can be anything!
It is causing you to have to make assertions further down in your code, such as having to add { id: number; }
when calling state.toDos.filter()
, which would not be necessary if your state
was properly typed.
It is also causing you to overlook errors such as having return;
in your default
case rather than return state;
. Those sorts of things should be picked up on by the typescript compiler, but it doesn't show an error in this case because undefined
is still assignable to your any
state type.
It looks like your state is an object with properties toDos
and completed
where both properties are array
s of Todo
objects. It seems as though you aren't actually using the done
property on the Todo
type, and are using the separate arrays to see which are done or not. I am not sure if you want that done
property added when we select the toDos from the state or if it's just a relic of old code and not needed.
interface Todo {
id: string;
text: string;
}
interface State {
toDos: Todo[];
completed: Todo[];
}
As far as your actions, you can get maximum type safety by defining the action type as the union of all the types for your specific actions. This is where it starts to feel like "this is such a headache, just use Redux Toolkit" as the toolkit really takes away so much of the boilerplate.
For most actions, it looks like the numeric id
of the toDo is your action.payload
. But for your edit action the id is action.id
and the text is the payload. I don't love this sort of inconsistency, but I'm just going to type what you have here rather than change the reducer.
type Action = {
type: typeof ADD | typeof DEL | typeof COMPLETE | typeof UNCOMPLETE;
payload: string;
} | {
type: typeof EDIT;
payload: string;
id: string;
}
As soon as I started adding types for the reducer a major error got highlighted that I had not noticed before! This is why proper typings are so important. Your switch
statement is switching based on action
when it should be on action.type
.
Right now your reducer takes one argument that is an object with properties state
and action
. But that is not what a reducer is and it will not work with useReducer
(or with redux) if you do not accept the right arguments. A reducer function looks like (state, action) => newState
.
const reducer = (state: State, action: Action): State
When I fix this, I start to see even more errors getting highlighted. It turns out that the id
which you are creating by calling uuidv4()
is a string
and not a number
. So everywhere that you typed a todo id as number
is wrong. But everywhere that you have (toDo: { id: number; })
in a callback, you can just change to toDo
because the type is known from the array.
There are errors in the complete, uncomplete, and edit cases when adding a target
to an array due to the possibility that no match was found and the target
is undefined
. We can make this conditional.
completed: target ? [...state.completed, { ...target }] : state.completed,
It's not great that we have to do the same thing in so many places. This sort of thing is where you might start to think about helper utility functions for altering an array toDos. Or again, everything is easier with Redux Toolkit.
In your original types you were saying that the context value is a single array of toDos which have a property done
. I'm not sure if you are intending to map from the state to a single array or if this is a mistake. I'm going to assume it's a mistake.
But if you want it in that format, it's:
const withDone = (state: State): Array<Todo & {done: boolean}> => {
return [
...state.toDos.map(todo => ({...todo, done: false})),
...state.completed.map(todo => ({...todo, done: true})),
]
}
We don't need to specify any types on useReducer
because those can all be inferred from our strongly-typed reducer
function. Woo! But we do need to specify the type of our context value.
interface ContextValue {
state: State;
dispatch: React.Dispatch<Action>;
}
const ToDosContext = createContext<ContextValue>(null);
Unless you have strictNullChecks
off in your tsconfig
, you'll probably get an error assigning null
as the initial value for the context as null
is not assignable to ContextValue
. So we have to give it an initial value, which is the value that will be used if the context is accessed without a Provider
to give a real value.
const ToDosContext = createContext<ContextValue>({
state: initialState,
dispatch: () => { console.error("called dispatch outside of a ToDosContext Provider")}
});
With our context typed, useTodosDispatch
and useTodosState
automatically infer the proper return types. Though I prefer to be explicit.
export const useTodosDispatch = (): React.Dispatch<Action> => { ... };
export const useTodosState = (): State => { ... };
Finally we have no more errors! As I added types to things, I found so many errors which had previously been hidden by all the any
. Here's the completed code:
import React, { createContext, useContext, useReducer } from "react";
import { v4 as uuidv4 } from "uuid";
import { ADD, DEL, COMPLETE, UNCOMPLETE, EDIT } from "./actions";
interface Todo {
id: string;
text: string;
}
interface State {
toDos: Todo[];
completed: Todo[];
}
type Action = {
type: typeof ADD | typeof DEL | typeof COMPLETE | typeof UNCOMPLETE;
payload: string;
} | {
type: typeof EDIT;
payload: string;
id: string;
}
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case ADD:
return {
...state,
toDos: [...state.toDos, { text: action.payload, id: uuidv4() }],
};
case DEL:
return {
...state,
toDos: state.toDos.filter((toDo) => toDo.id !== action.payload),
};
case COMPLETE:
const target = state.toDos.find((toDo) => toDo.id === action.payload);
return {
...state,
toDos: state.toDos.filter((toDo) => toDo.id !== action.payload),
completed: target ? [...state.completed, { ...target }] : state.completed,
};
case UNCOMPLETE:
const aTarget = state.completed.find(
(toDo) => toDo.id === action.payload
);
return {
...state,
toDos: aTarget ? [...state.toDos, { ...aTarget }] : state.toDos,
completed: state.completed.filter(
(complete) => complete.id !== action.payload
),
};
case EDIT:
const bTarget = state.toDos.find((toDo) => toDo.id === action.id);
const rest = state.toDos.filter((toDo) => toDo.id !== action.id);
return {
...state,
toDos: bTarget ? rest.concat({ ...bTarget, text: action.payload }) : rest,
};
default:
return state;
}
};
interface ContextValue {
state: State;
dispatch: React.Dispatch<Action>;
}
export const initialState = {
toDos: [],
completed: [],
};
const ToDosContext = createContext<ContextValue>({
state: initialState,
dispatch: () => { console.error("called dispatch outside of a ToDosContext Provider") }
});
const ToDosProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ToDosContext.Provider value={{ state, dispatch }}>
{children}
</ToDosContext.Provider>
);
};
export const useTodosDispatch = (): React.Dispatch<Action> => {
const { dispatch } = useContext(ToDosContext);
return dispatch;
};
export const useTodosState = (): State => {
const { state } = useContext(ToDosContext);
return state;
};