Home > OS >  useEffect hook doesn't rerender when state is updated
useEffect hook doesn't rerender when state is updated

Time:02-18

I'm buildind a React Calendar using Tailwind and Redux and I have a little problem. I have a form where I create a new event and I push this date in my state 'savedEvents'. Atfer that, I want to use events from my state to generate dinamically some spans but my useEffect hook can't rerender when my state is updated and I don't understand why.

GitHub repository for code: https://github.com/snnack123/Events-Calendar

My global state (/store/events.js):

import dayjs from 'dayjs'

const initialState = {
    monthIndex: dayjs().month(),
    compare: dayjs().month(),
    smallCalendarMonth: 0,
    daySelected: dayjs(),
    showEventModal: false,
    savedEvents: [],
}

export function eventsReducer(state = initialState, action) {
    switch (action.type) {
        case 'events/setMonthIndex':
            return { ...state, monthIndex: action.payload };
        case 'events/setCompare':
            return { ...state, compare: action.payload };
        case 'events/setDaySelected':
            return { ...state, daySelected: action.payload };
        case 'events/setShowModal':
            return { ...state, showEventModal: action.payload };
        case 'events/setNewEvent':
            return { ...state, ...state.savedEvents.push(action.payload) };
        default:
            return state;
    }
}

My submit form (/components/EventModal.js):

<form className='bg-white rounded-lg shadow-2xl w-1/4'>
                <header className='bg-gray-100 px-4 py-2 flex justify-between items-center'>
                    <span className='material-icons-outlined text-gray-400'>
                        drag_handle
                    </span>
                    <button onClick={setShowModal}>
                        <span className='material-icons-outlined text-gray-400'>
                            close
                        </span>
                    </button>
                </header>
                <div className='p-3'>
                    <div className="grid grid-cols-1/5 items-end gap-y-7">
                        <div></div>
                        <input
                            type="text"
                            name="title"
                            placeholder='Add title'
                            value={title}
                            onChange={(e) => setTitle(e.target.value)}
                            required
                            className='pt-3 border-0 text-gray-600 text-xl font-semibold pb-2 w-full border-b-2 
                            border-gray-200 focus:outline-none focus:ring-0 focus-blue-500'
                        />
                        <span className='material-icons-outlined text-gray-400'>
                            schedule
                        </span>
                        <p>{daySelected.format('dddd, MMMM DD')}</p>
                        <span className='material-icons-outlined text-gray-400'>
                            segment
                        </span>
                        <input
                            type="text"
                            name="description"
                            placeholder='Add a description'
                            value={description}
                            onChange={(e) => setDescription(e.target.value)}
                            required
                            className='pt-3 border-0 text-gray-600 pb-2 w-full border-b-2 
                            border-gray-200 focus:outline-none focus:ring-0 focus-blue-500'
                        />
                        <span className='material-icons-outlined text-gray-400'>
                            bookmark_border
                        </span>
                        <div className="flex gap-x-2">
                            {labelsClasses.map((lblClass, i) => (
                                <span key={i} className={`bg-${lblClass}-500 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer`}
                                    onClick={() => setSelectedLabel(lblClass)}>
                                    {selectedLabel === lblClass && (
                                        <span className="material-icons-outlined text-white text-sm">
                                            check
                                        </span>
                                    )}
                                </span>
                            ))}
                        </div>
                    </div>
                </div>
                <footer className='flex justify-end border-t p-3 mt-5 '>
                    <button type='button' className='bg-blue-500 hover:bg-blue-600 px-6 py-2 rounded text-white' onClick={saveEvent}>
                        Save
                    </button>
                </footer>
            </form>

Function which update my state from this form:

    const [title, setTitle] = useState('');
    const [description, setDescription] = useState('');

    const labelsClasses = [
        "indigo",
        "gray",
        "green",
        "blue",
        "red",
        "purple",
    ];

    const [selectedLabel, setSelectedLabel] = useState(labelsClasses[0]);

    function saveEvent() {
        let new_event = {
            title,
            description,
            label: selectedLabel,
            day: daySelected.valueOf(),
            id: Date.now(),
        };

        dispatch({ type: 'events/setNewEvent', payload: new_event });
        setShowModal();
    }

This is my useEffect that doesn't work (/components/Day.js)

    const savedEvents = useSelector((state) => state.events.savedEvents);
    const [dayEvents, setDayEvents] = useState([]);

    useEffect(() => {
        const events = savedEvents.filter(
            (evt) =>
                dayjs(evt.day).format("DD-MM-YY") === day.format("DD-MM-YY")
        );
        setDayEvents(events);
    }, [savedEvents, day]);

I want to use my state here:

                {dayEvents.map((evt, idx) => (
                    <div
                        key={idx}
                        className={`bg-${evt.label}-200 p-1 mr-3 text-gray-600 text-sm rounded mb-1 truncate`}
                    >
                        {evt.title}
                    </div>
                ))}

CodePudding user response:

The hook UseEffect is working fine, the source of the problem is this:

export function eventsReducer(state = initialState, action) {
    switch (action.type) {
        case 'events/setMonthIndex':
            return { ...state, monthIndex: action.payload };
        case 'events/setCompare':
            return { ...state, compare: action.payload };
        case 'events/setDaySelected':
            return { ...state, daySelected: action.payload };
        case 'events/setShowModal':
            return { ...state, showEventModal: action.payload };
        case 'events/setNewEvent':
            return { ...state, ...state.savedEvents.push(action.payload) }; // <== This will not trigger the re-rendering of your component
         default:
            return state;
    }
}

Your component will not re-render even the change in the state, because you mutated the array savedEvents and Redux will not know that there is a change in the array savedEvents.

Solution:

export function eventsReducer(state = initialState, action) {
  switch (action.type) {
      case 'events/setMonthIndex':
          return { ...state, monthIndex: action.payload };
      case 'events/setCompare':
          return { ...state, compare: action.payload };
      case 'events/setDaySelected':
          return { ...state, daySelected: action.payload };
      case 'events/setShowModal':
          return { ...state, showEventModal: action.payload };
      case 'events/setNewEvent':
          return { ...state, savedEvents: [...state.savedEvents, action.payload] }; // <== add this to trigger the re-rendering of your component
      default:
          return state;
  }
}

CodePudding user response:

...state.savedEvents.push(action.payload)

You're mutating your array, so it is the same array and doesn't trigger the effect. Create a new one instead.

savedEvents: [...state.savedEvents, action.payload]
  • Related