reactjs - Problemas con el contexto al actualizar un arreglo
Estoy encontrando un problema con el estado de mi contexto al actualizar un estado que es un arreglo. Específicamente, tengo un objeto que almacena los días seleccionados por el usuario, los cuales pueden ser eliminados uno por uno. Al eliminar un día, el estado de React no se actualiza y el hook useEffect no se ejecuta.
fechas en tipo
eliminar una fecha
eliminar la fecha de nuevo
aquí está mi código:
import { createContext, Dispatch, useContext, useEffect, useReducer } from "react";
// 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;
};
}
export const initialTypeState: IAfterSchoolType = {
id: 0,
name: "",
selectedDays: [],
value: 0
};
export const initialServiceState: IAfterSchoolService = {
id: 0,
name: "",
types: [],
};
export const initialAfterSchoolState: IAfterSchoolState = {
type: initialTypeState,
service: initialServiceState,
};
export const AppContext = createContext<IAppContext | undefined>(undefined);
// 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
}
};
case "selectType":
if (!payload.type) return state;
return { ...state, type: payload.type };
case "addSchedule":
if (!payload.date) return state;
const newDates = payload.date;
return {
...state,
type: {
...state.type,
selectedDays: [...state.type.selectedDays, newDates]
},
};
default:
return state;
}
}
// hooks/app-context.hook.ts
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext debe ser usado dentro de MyAppContext');
}
return context;
};
// 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>
)
}
Problema :
Al eliminar una fecha, el estado no se actualiza y el hook useEffect no se dispara como se esperaba. ¿Qué podría estar causando este problema y cómo puedo asegurarme de que el estado se actualice correctamente y se dispare el useEffect?
¡Cualquier ayuda o idea sería muy apreciada!
Respuesta #1
React puede no causar re-renderizados y, por lo tanto, el useEffect no se activa si estás ejecutando dos acciones diferentes casi al mismo tiempo sobre el mismo objeto. Recomendaría refactorizar tu base de código para realizar una sola acción sobre el arreglo en lugar de dos consecutivas:
une tus acciones en una sola
// 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);
const newTypes = state.service.types.map(t =>
(t.id === state.type.id)
? { ...state.type, selectedDays: updatedSelectedDays }
: t
);
return {
...state,
type: {
...state.type,
selectedDays: updatedSelectedDays,
},
service: {
...state.service,
types: newTypes
}
};
}
default:
return state;
}
};
gancho refactorizado
// hooks/after-school.ts
export const useAfterSchool = () => {
const { afterSchool: { type }, setAfterSchool } = useAppContext();
const handleDateRemove = (date: string) => {
setAfterSchool({ type: "removeSchedule", payload: { removeDate: date } });
};
useEffect(() => {
console.log("type:", type);
}, [type.selectedDays]); // React will re-trigger on `selectedDays` change
}
Tenga en cuenta también que useEffect ahora depende de type.selectedDays para volver a activarse.
Respuesta #2
El useEffect se está activando correctamente. Pero el problema ocurre cuando agregas la misma fecha varias veces y usas la misma clave para varios elementos de la lista
// In the App3 component
{type.selectedDays.map(selectedDay => (
<p key={selectedDay} onClick={() => handleDateRemove(selectedDay)}>{formatDayMonth(selectedDay, "/")}</p>))}
En lugar de
{type.selectedDays.map((selectedDay, index) => (
<p key={`${selectedDay}${index}`} onClick={() => handleDateRemove(selectedDay)}>{formatDayMonth(selectedDay, "/")}</p>))}
Hay mejores maneras de hacerlo, solo señalé la raíz del problema
