I get this error message:
src/questions/QuestionComponent.tsx Line 22:8: React Hook useEffect has a missing dependency: 'setWorkdayAnswer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
The purpose of the code concerning the problem is that I have checkboxes from Monday to Friday and every time any checkbox gets hit, the state information of all checkboxes should be sent to the backend via postWorkdayAnswer()
.
Then I do npm start
everything works fine, no bugs in executing the frontend whatsoever, but if run npm run build
I get the above error, which prevents my CI pipeline to build successfully.
This is the code:
import { Question, TimeAnswer, TimeUnit, WorkdayAnswer } from "../service/models";
import { useEffect, useState } from "react";
import { getTimeUnitList, postTimeAnswer, postWorkdayAnswer } from "../service/apiService";
import TimeAnswerProperties from "./TimeAnswerProperties";
import "./QuestionComponent.css"
import { convertTimeUnitToMinutes } from "../utilities/Util"
interface QuestionProps {
question: Question
answers: Array<TimeAnswer>
answerCallback: () => void // refreshing site
}
export default function QuestionListComponent(props: QuestionProps) {
const [timeUnitList, setTimeUnitList] = useState<Array<TimeUnit>>([])
const [currentTimeAnswer, setCurrentTimeAnswer] = useState<string>()
const [workdays, setWorkdays] = useState<boolean[]>([true, true, true, true, true, false, false]);**
const [errorMessage, setErrorMessage] = useState("")
useEffect(() => {
setWorkdayAnswer();
}, [workdays]);
const setWorkdayAnswer = () => {
const workdayAnswer: WorkdayAnswer = {
questionId: props.question.id,
question: props.question.question,
monday: workdays[0],
tuesday: workdays[1],
wednesday: workdays[2],
thursday: workdays[3],
friday: workdays[4],
saturday: workdays[5],
sunday: workdays[6],
};
postWorkdayAnswer(workdayAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting workday answer:", Error);
setErrorMessage("error posting answer");
})
};
useEffect(() => {
const loadTimeUnitList = async () => {
try {
const data = await getTimeUnitList();
setTimeUnitList(data);
} catch {
setErrorMessage("Failed to load time unit list")
}
};
loadTimeUnitList();
const currentAnswer = props.answers.find(answer => answer.questionId === props.question.id)
setCurrentTimeAnswer(currentAnswer ? currentAnswer.time : "00:00");
}, [props.answers, props.question.id])
const setTimeAnswer = (timeInMinutes: number) => {
const timeAnswer: TimeAnswer = {
questionId: props.question.id,
question: props.question.question,
timeInMinutes: timeInMinutes
}
postTimeAnswer(timeAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting time answer:", Error);
setErrorMessage("error posting answer");
})
};
const timeUnitsToChoose = timeUnitList
.filter(timeUnit => {
if (!props.question.previousQuestionId) {
return true;
} else {
const previousQuestionAnswer = props.answers.find(answer => answer.questionId === props.question.previousQuestionId)
if (previousQuestionAnswer) {
const currentTimeAnswerInMinutes = convertTimeUnitToMinutes(currentTimeAnswer);
return (
timeUnit.timeInMinutes! >= previousQuestionAnswer.timeInMinutes) &&
timeUnit.timeInMinutes !== currentTimeAnswerInMinutes;
} else {
return true;
}
}
}
)
.map(timeUnit => <TimeAnswerProperties key={timeUnit.id} timeUnit={timeUnit}/>)
const renderWorkdayCheckboxes = () => {
const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
return days.map((day, index) => (
<span key={index}>
<input
type="checkbox"
id={`check${index}`}
checked={workdays[index]}
onChange={event => {
const newWorkdays = [...workdays];
newWorkdays[index] = event.target.checked;
setWorkdays(newWorkdays);
}}
/>
<label htmlFor={`check${index}`}>{day}</label>
</span>
));
};
const QuestionType = () => {
if (props.question.question === "On which days do you work ?") {
return (
<div>
{renderWorkdayCheckboxes()}
</div>
);
} else {
return (
<div>
<select
className="questionAnswer"
name={props.question.type}
id={props.question.id}
onChange={event => setTimeAnswer(Number(event.target.value))}
>
value=<TimeAnswerProperties
key={currentTimeAnswer}
timeUnit={{
time: currentTimeAnswer + "",
length: 15,
end: "24:00"
}}
/>
{timeUnitsToChoose}
</select>
</div>
);
}
};
return (
<div className="question">
<p>{props.question.question}</p>
{errorMessage && <div>{errorMessage}</div>}
{QuestionType()}
</div>
);
}
I did try adding setWorkdayAnswer
as a dependency in
useEffect(() => {
setWorkdayAnswer();
}, [workdays]);
which causes weird bugs that days are not being saved right are the last one is missing or something like that.
I also tried adding setWorkdays
, which also causes weird bugs.
Solution:
Parent Component "Question.tsx" with useCallback after Drew's feedback:
import {NavLink} from "react-router-dom";
import {useEffect, useState, useCallback} from "react";
import {getQuestionList, getTimeAnswer} from "../service/apiService";
import QuestionListComponent from "./QuestionComponent";
import {Question, TimeAnswer} from "../service/models";
import "./Questions.css"
export default function QuestionList() {
const [questionList, setQuestionList] = useState<Array<Question>>([])
const [errorMessage, setErrorMessage] = useState("")
const [answers, setAnswers] = useState<Array<TimeAnswer>>([])
useEffect(() => {
getQuestionList()
.then(data => setQuestionList(data))
.catch(() => setErrorMessage("questionList doesnt load"));
}, [])
const onAnswer = useCallback(() => {
/* getQuestionList()
.then(data => setQuestionList(data))
.catch(() => setErrorMessage("questionList doesnt load")); */
getTimeAnswer()
.then(data => setAnswers(data))
.catch(() => setErrorMessage("timeAnswer doesnt load"));
}, []);
const questions = questionList.sort((s1, s2) => s1.order - s2.order).map(question => <QuestionListComponent
key={question.id} question={question} answers={answers} answerCallback={onAnswer}/>)
return (
<div className="body">
<div className="questions">
<h1 className="questionHeadline"> Questions:</h1>
{errorMessage && <div>{errorMessage}</div>}
{questions}
<br/>
<br/>
<NavLink to={"/timetable"}>
<button className="createButton">create</button>
</NavLink>
</div>
</div>
)
}
fixed "QuestionComponent.tsx":
import {Question, TimeAnswer, TimeUnit, WorkdayAnswer} from "../service/models";
import {useEffect, useState} from "react";
import {getTimeUnitList, postTimeAnswer, postWorkdayAnswer} from "../service/apiService";
import TimeAnswerProperties from "./TimeAnswerProperties";
import "./QuestionComponent.css"
import {convertTimeUnitToMinutes} from "../utilities/Util"
interface QuestionProps {
question: Question
answers: Array<TimeAnswer>
answerCallback: () => void //refreshing site
}
export default function QuestionListComponent(props: QuestionProps) {
const [timeUnitList, setTimeUnitList] = useState<Array<TimeUnit>>([])
const [currentTimeAnswer, setCurrentTimeAnswer] = useState<string>()
const [workdays, setWorkdays] = useState<boolean[]>([true, true, true, true, true, false, false]);
const [errorMessage, setErrorMessage] = useState("")
const question = props.question
const answerCallback = props.answerCallback
useEffect(() => {
const workdayAnswerDbUpdate = () => {
const workdayAnswer: WorkdayAnswer = {
questionId: question.id,
question: question.question,
monday: workdays[0],
tuesday: workdays[1],
wednesday: workdays[2],
thursday: workdays[3],
friday: workdays[4],
saturday: workdays[5],
sunday: workdays[6],
};
postWorkdayAnswer(workdayAnswer)
.then(() => answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting workday answer:", Error);
setErrorMessage("error posting answer");
});
};
workdayAnswerDbUpdate();
}, [answerCallback, question, workdays]);
useEffect(() => {
const loadTimeUnitList = async () => {
try {
const data = await getTimeUnitList();
setTimeUnitList(data);
} catch {
setErrorMessage("Failed to load time unit list")
}
};
loadTimeUnitList();
const currentAnswer = props.answers.find(answer => answer.questionId === props.question.id)
setCurrentTimeAnswer(currentAnswer ? currentAnswer.time : "00:00");
}, [props.answers, props.question.id])
const timeAnswerDbUpdate = (timeInMinutes: number) => {
const timeAnswer: TimeAnswer = {
questionId: props.question.id,
question: props.question.question,
timeInMinutes: timeInMinutes
}
postTimeAnswer(timeAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting time answer:", Error);
setErrorMessage("error posting answer");
})
};
const timeUnitsToChoose = timeUnitList
.filter(timeUnit => {
if (!props.question.previousQuestionId) {
return true;
} else {
const previousQuestionAnswer = props.answers.find(answer => answer.questionId === props.question.previousQuestionId)
if (previousQuestionAnswer) {
const currentTimeAnswerInMinutes = convertTimeUnitToMinutes(currentTimeAnswer);
return (
timeUnit.timeInMinutes! >= previousQuestionAnswer.timeInMinutes) &&
timeUnit.timeInMinutes !== currentTimeAnswerInMinutes;
} else {
return true;
}
}
})
.map(timeUnit => <TimeAnswerProperties key={timeUnit.id} timeUnit={timeUnit}/>)
const renderWorkdayCheckboxes = () => {
const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
return days.map((day, index) => (
<span key={index}>
<input
type="checkbox"
id={`check${index}`}
checked={workdays[index]}
onChange={event => {
const newWorkdays = [...workdays];
newWorkdays[index] = event.target.checked;
setWorkdays(newWorkdays);
}}
/>
<label htmlFor={`check${index}`}>{day}</label>
</span>
));
};
const QuestionType = () => {
if (props.question.question === "On which days do you work ?") {
return (
<div>
{renderWorkdayCheckboxes()}
</div>
);
}
else{
return(
<div>
<select
className="questionAnswer"
name={props.question.type}
id={props.question.id}
onChange={event => timeAnswerDbUpdate(Number(event.target.value))}
>
value=<TimeAnswerProperties
key={currentTimeAnswer}
timeUnit={{
time: currentTimeAnswer + "",
length: 15,
end: "24:00"
}}
/>
{timeUnitsToChoose}
</select>
</div>
);
}
};
return (
<div className="question">
<p>{props.question.question}</p>
{errorMessage && <div>{errorMessage}</div>}
{QuestionType()}
</div>
);
}
setWorkdayAnswer
is missing as a useEffect
hook dependency since it's an external (to the hook callback) reference. workdays
is indirectly a dependence since setWorkdayAnswer
depends on it.
You've a few options:
Memoize setWorkdayAnswer
on workdays
, answerCallback
, and question
as dependencies via useCallback
and use setWorkdayAnswer
as the useEffect
hook dependency
const { answerCallback, question } = props;
...
const setWorkdayAnswer = useCallback(() => {
const workdayAnswer: WorkdayAnswer = {
questionId: props.question.id,
question: props.question.question,
monday: workdays[0],
tuesday: workdays[1],
wednesday: workdays[2],
thursday: workdays[3],
friday: workdays[4],
saturday: workdays[5],
sunday: workdays[6],
};
postWorkdayAnswer(workdayAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting workday answer:", Error);
setErrorMessage("error posting answer");
});
}, [answerCallback, question, workdays]);
useEffect(() => {
setWorkdayAnswer();
}, [setWorkdayAnswer]);
Move setWorkdayAnswer
into the useEffect
callback body to eliminate it as an external dependency.
const { answerCallback, question } = props;
...
useEffect(() => {
const setWorkdayAnswer = () => {
const workdayAnswer: WorkdayAnswer = {
questionId: props.question.id,
question: props.question.question,
monday: workdays[0],
tuesday: workdays[1],
wednesday: workdays[2],
thursday: workdays[3],
friday: workdays[4],
saturday: workdays[5],
sunday: workdays[6],
};
postWorkdayAnswer(workdayAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting workday answer:", Error);
setErrorMessage("error posting answer");
});
};
setWorkdayAnswer();
}, [answerCallback, question, workdays]);
Just apply the fetching logic directly.
const { answerCallback, question } = props;
...
useEffect(() => {
const workdayAnswer: WorkdayAnswer = {
questionId: props.question.id,
question: props.question.question,
monday: workdays[0],
tuesday: workdays[1],
wednesday: workdays[2],
thursday: workdays[3],
friday: workdays[4],
saturday: workdays[5],
sunday: workdays[6],
};
postWorkdayAnswer(workdayAnswer)
.then(() => props.answerCallback()) // refreshing site
.catch(() => {
console.error("Error posting workday answer:", Error);
setErrorMessage("error posting answer");
});
};
}, [answerCallback, question, workdays]);
Note that if answerCallback
is passed down as a prop that it should also be memoized and passed as a stable reference. In the parent you would also use the useCallback
hook for this.
Example:
const answerCallback = useCallback(() => {
// ... callback logic
}, [/* appropriate dependencies */]);
...
return (
...
<QuestionListComponent
answerCallback={answerCallback}
...
/>
...
);