Typescript useReducer error solved when adding new state
So, this is just for my reference. previously I had this code.
import { useEffect, useReducer } from "react";
import { questionsApi } from "../constants";
import { Loader } from "../components/Loader";
import { Error } from "../components/Error";
import { StartScreen } from "../components/StartScreen";
import { Question } from "../components/Question";
interface Question {
correctOption: number;
id: number;
options: string[];
points: number;
question: string;
}
export enum ActionType {
DATA_RECEIVED,
DATA_FAILED,
START,
}
interface State {
questions: [] | Question[];
status: QuestionsStatus;
}
enum QuestionsStatus {
LOADING,
READY,
ERROR,
ACTIVE,
FINISHED,
}
type Action =
| { type: ActionType.DATA_RECEIVED; payload: Question[] }
| { type: ActionType.DATA_FAILED }
| { type: ActionType.START };
const initialState: State = {
questions: [],
status: QuestionsStatus.LOADING,
};
const reducer = (state: State, action: Action) => {
const { type } = action;
switch (type) {
case ActionType.DATA_RECEIVED:
return { questions: action.payload, status: QuestionsStatus.READY };
case ActionType.DATA_FAILED:
return { ...state, status: QuestionsStatus.ERROR };
case ActionType.START:
return { ...state, status: QuestionsStatus.ACTIVE };
default:
return { ...state };
}
};
export const QuizContext = () => {
const [{ questions, status }, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchUsers = async () => {
try {
const resp = await fetch(questionsApi);
const questions = await resp.json();
dispatch({ type: ActionType.DATA_RECEIVED, payload: questions });
} catch (error) {
dispatch({ type: ActionType.DATA_FAILED });
}
};
fetchUsers();
}, []);
return (
<>
{status === QuestionsStatus.LOADING && <Loader />}
{status === QuestionsStatus.ERROR && <Error />}
{status === QuestionsStatus.READY && (
<StartScreen numQuestions={questions.length} dispatch={dispatch} />
)}
{status === QuestionsStatus.ACTIVE && <Question />}
</>
);
};
I wanted to add a new state called answer
in the initialState
and its type definition. As soon as I added this new state in initialState
, I got a typescript error inside the component where I was using reducer
. The error was:
No overload matches this call.
Overload 1 of 5, '(reducer: ReducerWithoutAction<any>, initializerArg: any, initializer?: undefined): [any, DispatchWithoutAction]', gave the following error.
Argument of type '(state: State, action: Action) => { questions: Question[]; status: QuestionsStatus; } | { status: QuestionsStatus; questions: [] | Question[]; answer: number; }' is not assignable to parameter of type 'ReducerWithoutAction<any>'.
Target signature provides too few arguments. Expected 2 or more, but got 1.
Overload 2 of 5, '(reducer: (state: State, action: Action) => { questions: Question[]; status: QuestionsStatus; } | { status: QuestionsStatus; questions: [] | Question[]; answer: number; }, initialState: never, initializer?: undefined): [...]', gave the following error.
Argument of type 'State' is not assignable to parameter of type 'never'.ts(2769)
The second place where I got the error was wherever I was using the dispatch
function.
The code after adding the new state was:
import { useEffect, useReducer } from "react";
import { questionsApi } from "../constants";
import { Loader } from "../components/Loader";
import { Error } from "../components/Error";
import { StartScreen } from "../components/StartScreen";
import { Question } from "../components/Question";
interface Question {
correctOption: number;
id: number;
options: string[];
points: number;
question: string; // question?: string fixes all TS errors.
}
export enum ActionType {
DATA_RECEIVED,
DATA_FAILED,
START,
NEW_ANSWER,
}
type State = {
questions: [] | Question[];
status: QuestionsStatus;
answer: number;
};
enum QuestionsStatus {
LOADING,
READY,
ERROR,
ACTIVE,
FINISHED,
}
type Action =
| { type: ActionType.DATA_RECEIVED; payload: Question[] }
| { type: ActionType.DATA_FAILED }
| { type: ActionType.START }
| { type: ActionType.NEW_ANSWER; payload: number };
const initialState: State = {
questions: [],
status: QuestionsStatus.LOADING,
answer: 0,
};
const reducer = (state: State, action: Action) => {
const { type } = action;
switch (type) {
case ActionType.DATA_RECEIVED:
return { questions: action.payload, status: QuestionsStatus.READY };
case ActionType.DATA_FAILED:
return { ...state, status: QuestionsStatus.ERROR };
case ActionType.START:
return { ...state, status: QuestionsStatus.ACTIVE };
case ActionType.NEW_ANSWER:
return { ...state, answer: action.payload };
default:
return { ...state };
}
};
export const QuizContext = () => {
// got the error here on useReducer
const [{ questions, status }, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const fetchUsers = async () => {
try {
const resp = await fetch(questionsApi);
const questions = await resp.json();
dispatch({ type: ActionType.DATA_RECEIVED, payload: questions });
} catch (error) {
dispatch({ type: ActionType.DATA_FAILED });
}
};
fetchUsers();
}, []);
return (
<>
<button
onClick={() => dispatch({ type: ActionType.NEW_ANSWER, payload: 1 })}
>
New Answer {}
</button>
{status === QuestionsStatus.LOADING && <Loader />}
{status === QuestionsStatus.ERROR && <Error />}
{status === QuestionsStatus.READY && (
<StartScreen numQuestions={questions.length} dispatch={dispatch} />
)}
{status === QuestionsStatus.ACTIVE && <Question />}
</>
);
};
How I fixed it ?(temporary)
I just added a conditional type for this new state. I just added a question mark. I want to know how this works.
type State = {
questions: [] | Question[];
status: QuestionsStatus;
answer?: number;
};
How I fixed it ?(Really)
Do you see the first switch condition !? I didn't spread the previous state. So if you spread the previous state and override with adding new state properties, this whole mess goes away. Here is the code:
const reducer = (state: State, action: Action) => {
const { type } = action;
switch (type) {
case ActionType.DATA_RECEIVED:
return { ...state, questions: action.payload, status: QuestionsStatus.READY };
case ActionType.DATA_FAILED:
return { ...state, status: QuestionsStatus.ERROR };
case ActionType.START:
return { ...state, status: QuestionsStatus.ACTIVE };
case ActionType.NEW_ANSWER:
return { ...state, answer: action.payload };
default:
return { ...state };
}
};