I’m encountering an issue with the state of my context when updating a state that is an array. Specifically, I have an object that stores days selected by the user, which can be removed one by one. When removing a day, the React state does not update, and the useEffect hook is not executed.
here is my code :
// mock/after-school.ts
export const GetDailyServicesResponse = {
"serviceId": 116,
"name": "After school",
"description": "After school",
"bookingId": 62,
"bookingServiceId": 82,
"bookingCalendarId": 22,
"services": [
{
"id": 1,
"name": "first service",
"value": 23200,
"lunch": 0,
"ticket": 1
},
{
"id": 2,
"name": "second service",
"value": 29200,
"lunch": 1,
"ticket": 1
},
{
"id": 3,
"name": "third service",
"value": 6000,
"lunch": 1,
"ticket": 0
}
]
}
export const MockDates = [
new Date("11-02-2024"),
new Date("11-03-2024"),
new Date("11-04-2024"),
new Date("11-05-2024"),
new Date("11-06-2024"),
new Date("11-07-2024"),
new Date("11-08-2024"),
new Date("11-09-2024"),
new Date("11-10-2024"),
new Date("11-11-2024"),
]
//utils/date.ts
export const formatDayMonth = (value: string, symbol: string) => {
const date = new Date(value);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${day}${symbol}${month}`;
}
// interfaces/context.ts
export interface IAppContext {
afterSchool: IAfterSchoolState;
setAfterSchool: Dispatch<IAfterSchoolAction>
}
// interfaces/after-school.ts
export interface IAfterSchoolType {
id: number;
name: string;
value: number;
selectedDays: string[];
}
export interface IAfterSchoolService {
id: number;
name: string;
types: IAfterSchoolType[];
}
export interface IAfterSchoolState {
type: IAfterSchoolType;
service: IAfterSchoolService;
}
export type TAfterSchoolActionType =
| "addSchedule" | "removeSchedule" | "addAvailableDate"
| "selectType" | "selectPerson" | "selectDay" | "selectModality"
| "initService" | "updateServiceTypes";
export interface IAfterSchoolAction {
type: TAfterSchoolActionType;
payload: {
service?: IAfterSchoolService;
date?: string;
removeDate?: string;
availableDate?: number;
type?: IAfterSchoolType;
};
}
// context/index.tsx
export const MyAppContext = ({children}: any) => {
const [afterSchool, setAfterSchool] = useReducer(afterSchoolReducer, initialAfterSchoolState);
return (
<AppContext.Provider value={{afterSchool, setAfterSchool}}>
{children}
</AppContext.Provider>
)
}
// context/reducer/after-school.reducer.ts
export const afterSchoolReducer = (state: IAfterSchoolState, action: IAfterSchoolAction): IAfterSchoolState => {
const { type, payload } = action;
switch (type) {
case 'removeSchedule': {
const { removeDate } = payload;
if (!removeDate) return state;
const updatedSelectedDays = state.type.selectedDays.filter(date => date !== removeDate);
return {
...state,
type: {
...state.type,
selectedDays: [...updatedSelectedDays],
},
};
};
case "updateServiceTypes":
console.log("updateServiceTypes type:", state.type)
const newTypes = state.service.types.map(type =>
(type.id === state.type.id)
? state.type
: type
);
return {
...state,
service: {
...state.service,
types: newTypes
}
};
default:
return state;
}
// hooks/after-school.ts
export const useAfterSchool = () => {
const { afterSchool: { type, service }, setAfterSchool } = useAppContext();
const getServiceList = () => {
const resp = GetDailyServicesResponse;
const data: IAfterSchoolService = {
id: resp.serviceId,
name: resp.name,
types: resp.services.map(val => ({
id: val.id,
name: val.name,
value: val.value,
selectedDays: [],
}))
}
setAfterSchool({ type: "initService", payload: { service: data } });
};
const handleSelectService = (id: number) => {
const findService = service.types.find(type => type.id === id);
if (!findService) return;
setAfterSchool({ type: "updateServiceTypes", payload: {} });
setAfterSchool({ type: "selectType", payload: { type: findService } });
}
const handleDateSelect = (date: Date) => {
setAfterSchool({ type: "addSchedule", payload: { date: date.toString() } });
setAfterSchool({ type: "updateServiceTypes", payload: {} });
};
const handleDateRemove = (date: string) => {
setAfterSchool({ type: "removeSchedule", payload: { removeDate: date } });
// setAfterSchool({ type: "updateServiceTypes", payload: {} });
};
useEffect(() => {
getServiceList();
}, []);
useEffect(() => {
console.log("type:", type);
}, [type]);
return {
selectedType: type,
service,
handleDateSelect,
handleDateRemove,
handleSelectService,
}
}
//App.tsx
export const App3 = () => {
const {selectedType, service, handleSelectService, handleDateSelect, handleDateRemove} = useAfterSchool();
return (
<main className="mainContainer">
<article>
{service.types.map(type => (
<div
key={type.id}
className={`serviceCard ${selectedType.id === type.id ? "isSelected" : ""}`}
onClick={() => handleSelectService(type.id)}
>
{type.name} {type.value}
{type.selectedDays.map(selectedDay => (
<p key={selectedDay} onClick={() => handleDateRemove(selectedDay)}>{formatDayMonth(selectedDay, "/")}</p>
))}
</div>
))}
</article>
<article>
{MockDates.map((day, idx) => (
<div
key={idx}
onClick={() => handleDateSelect(day)}
className="serviceCard"
>
{formatDayMonth(day.toString(), "/")}
</div>
))}
</article>
</main>
)
}
Issue :
When removing a date, the state does not update, and the useEffect hook is not triggered as expected. What could be causing this issue, and how can I ensure the state updates correctly and triggers the useEffect?
Any help or insights would be greatly appreciated!
2
Answers
React may not cause re-renders and hence the
useEffect
does not trigger if you are executing two different actions at almost the same time on the very same object. I would recommend refactoring your codebase to perform a single action over the array instead of two consequent ones:join your actions into one
refactored hook
Please also note that useEffect now relies on
type.selectedDays
to re-trigger.The useEffect is triggering just fine. But the issue occurs when you add same date multiple time and using the same key for multiple list items
Instead
There are better ways of doing it, just pointed the root cause