reactjs - Problem med context vid uppdatering av en array


Jag stöter på ett problem med tillståndet i mitt sammanhang när jag uppdaterar ett tillstånd som är en array. Specifikt har jag ett objekt som lagrar dagar som användaren har valt, vilka kan tas bort en efter en. När jag tar bort en dag uppdateras inte React-tillståndet, och useEffect-hooken körs inte.

datum i typ

ta bort ett datum

ta bort datum igen

här är min kod :

    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>
  )
}

Problem :

När man tar bort ett datum uppdateras inte tillståndet, och useEffect-hooken utlöses inte som förväntat. Vad kan orsaka detta problem, och hur kan jag säkerställa att tillståndet uppdateras korrekt och triggar useEffect?

All hjälp eller insikter skulle uppskattas enormt!

Svar #1

React kanske inte orsakar omrenderingar och därför triggar inte useEffect om du utför två olika åtgärder nästan samtidigt på exakt samma objekt. Jag skulle rekommendera att refaktorera din kodbas för att utföra en enda åtgärd över arrayen istället för två på varandra följande:

// 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;
  }
};

omstrukturerad krok

// 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
}

Observera också att useEffect nu är beroende av type.selectedDays för att triggas igen.

Svar #2

useEffect triggas helt fint. Men problemet uppstår när du lägger till samma datum flera gånger och använder samma nyckel för flera listobjekt

// In the App3 component 
{type.selectedDays.map(selectedDay => (
  <p key={selectedDay} onClick={() => handleDateRemove(selectedDay)}>{formatDayMonth(selectedDay, "/")}</p>))}

Istället

  {type.selectedDays.map((selectedDay, index) => (
 <p key={`${selectedDay}${index}`} onClick={() => handleDateRemove(selectedDay)}>{formatDayMonth(selectedDay, "/")}</p>))}

Det finns bättre sätt att göra det på, pekade bara ut grundorsaken