skip to Main Content

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.

dates in type

remove a date

remove date again

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


  1. 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

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

    refactored hook

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

    Please also note that useEffect now relies on type.selectedDays to re-trigger.

    Login or Signup to reply.
  2. 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

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

    Instead

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

    There are better ways of doing it, just pointed the root cause

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search