Search code examples
javascriptreactjsrenderuse-effect

Why React not re-rendering components even if it's state changes after an API call


I have a React state like this initialized with an empty array when component initialises

const [upcomingDates, setUpcomingDates] = useState([]);

And this calendar component depends on this state. (This is a chld component of our main component and has this state dependency)

<Calender name="Eat Eggs" dates={upcomingDates} />

Once component loads, I call an API and fills the array once API promise resolves

async function refreshPattern()
{
    //Call the API
    let pattern = await GetRepeatPattern(id);
    //Update State
    setUpcomingDates(pattern.UpcomingDates);

    setNextOccurance(pattern.NextOccurance);       
}

This is my useEffect

useEffect(async () => {
 refreshPattern();
},[]);

Now the issue is

  • The callender component is rendering only once with that empty array in it's props
  • React is not re-rendering even after I change state
  • When I put 'upcomingDates' inside useEffect(...,[upcomingDates]) second parameter, React falls to an infinite loop

This is the API response from console .logging the 'pattern' variable enter image description here

How to make the Calendar component re-render? And why react is not re-rendering component even if I changed my state?

NOTE: This is the full component.

export const RepeatSelector = ({ id }) => {

    let dropdownPatternMode = null;


    const [every, setEvery] = useState(1);
    const [timeMode, setTimeMode] = useState(1);
    const [course, setCourse] = useState(1);
    const [constrain, setConstrain] = useState(1);
    const [patternMode, setPatternMode] = useState(1);
    const [nextOccurance, setNextOccurance] = useState('Loading...');
    const [upComingDates, setUpcomingDates] = useState([]);
    const [dayOfMonth, setDayOfMonth] = useState(1);
    const [logicalStart, setLogicalStart] = useState(1);
    const [logicalDay, setLogicalDay] = useState(1);
    const [pattern, setPattern] = useState({
        Value: '',
        Mode: '',
        Constrain: '',
        Time: '',
        Date: ''
    });
    const [startDay, setStartDay] = useState(new Date());
    const [startTime, setStartTime] = useState('00:00');
    const [days, setDays] = useState([
        {
            selected: false,
            name: "SUN"
        },
        {
            selected: false,
            name: "MON"
        },
        {
            selected: true,
            name: "TUE"
        },
        {
            selected: true,
            name: "WED"
        },
        {
            selected: false,
            name: "THU"
        },
        {
            selected: false,
            name: "FRI"
        },
        {
            selected: false,
            name: "SAT"
        },
    ]);


    function init() {
        dropdownPatternMode = new Choices(document.getElementById('everyCount'), { searchEnabled: false, shouldSort: false });
    }


    useEffect(async function () {
        init();
        let response = await GetActivityRepeat(id);
        setDayOfMonth(response.DayOfMonth);
        
        days.map((day, index) => {
            if (day.name == 'SUN') {
                days[index] = { selected: response.Sunday, name: 'SUN' };
            }
            else if (day.name == 'MON') {
                days[index] = { selected: response.Monday, name: 'MON' };
            }
            else if (day.name == 'TUE') {
                days[index] = { selected: response.Tuesday, name: 'TUE' };
            }
            else if (day.name == 'WED') {
                days[index] = { selected: response.Wednesday, name: 'WED' };
            }
            else if (day.name == 'THU') {
                days[index] = { selected: response.Thursday, name: 'THU' };
            }
            else if (day.name == 'FRI') {
                days[index] = { selected: response.Friday, name: 'FRI' };
            }
            else if (day.name == 'SAT') {
                days[index] = { selected: response.Saturday, name: 'SAT' };
            }
            return days;
        });

        setDays(days);

        setLogicalStart(response.LogicalStart);
        setLogicalDay(response.LogicalDay);
        setCourse(response.Course);
        setEvery(response.PatternValue);

        setPatternMode(response.PatternMode);
        dropdownPatternMode.setChoiceByValue([response.PatternMode.toString()]);
        setStartDay(new Date(response.StartBy));
        setConstrain(response.Constrain);
        let myTime = response.SpecificTime.split('T')[1];
        setStartTime(myTime);
        

        async function refreshPattern()
        {
            //Call the API
            let pattern = await GetRepeatPattern(id);
            //Update State
            setUpcomingDates(pattern.UpcomingDates);
            setPattern(pattern);
            setNextOccurance(pattern.NextOccurance);       
        }
        refreshPattern();

        setTimeMode(response.TimeMode);
    }, []);

    async function SaveData() {
        let sDate = new Date().toISOString().substring(0, 10) + " 00:00";
        let sTime = new Date().toISOString().substring(0, 10) + " 00:00";

        try {
            sDate = startDay.toISOString().substring(0, 10) + " 00:00";
        }
        catch
        {
            store.addNotification({
                title: "Start Date Required",
                message: "Please choose a start date for this activity",
                type: "warning",
                insert: "top",
                container: "top-right",
                animationIn: ["animate__animated", "animate__fadeIn"],
                animationOut: ["animate__animated", "animate__fadeOut"],
                dismiss: {
                    duration: 2000,
                    pauseOnHover: true
                }
            });
            return;
        }

        try {
            sTime = new Date().toISOString().substring(0, 10) + " " + startTime;
        }
        catch
        {
            if (timeMode == 2) {
                store.addNotification({
                    title: "Start Time Required",
                    message: "Please choose a start time for this activity",
                    type: "warning",
                    insert: "top",
                    container: "top-right",
                    animationIn: ["animate__animated", "animate__fadeIn"],
                    animationOut: ["animate__animated", "animate__fadeOut"],
                    dismiss: {
                        duration: 2000,
                        pauseOnHover: true
                    }
                });
                return;
            }
        }

        var data = {
            ActivityId: parseInt(id),
            Course: course,
            PatternValue: every,
            PatternMode: patternMode,
            Sunday: days.find((e) => e.name == 'SUN').selected,
            Monday: days.find((e) => e.name == 'MON').selected,
            Tuesday: days.find((e) => e.name == 'TUE').selected,
            Wednesday: days.find((e) => e.name == 'WED').selected,
            Thursday: days.find((e) => e.name == 'THU').selected,
            Friday: days.find((e) => e.name == 'FRI').selected,
            Saturday: days.find((e) => e.name == 'SAT').selected,
            Constrain: constrain,
            DayOfMonth: dayOfMonth,
            LogicalStart: logicalStart,
            LogicalDay: logicalDay,
            StartBy: sDate,
            TimeMode: timeMode,
            SpecificTime: sTime,
        };
        await SaveActivityRepeat(data);
        store.addNotification({
            title: "Activity Saved",
            message: "Data saved successfully. Changes will reflect soon",
            type: "success",
            insert: "top",
            container: "top-right",
            animationIn: ["animate__animated", "animate__fadeIn"],
            animationOut: ["animate__animated", "animate__fadeOut"],
            dismiss: {
                duration: 2000,
                pauseOnHover: true
            }
        });
        //refreshPattern();
    }


    const renderRepeatPattern = () => {
        return (
            <>
                <ul className="list-group mt-3">
                    <li className="list-group-item border-0 d-flex p-4 mb-2 bg-gray-100 border-radius-lg">
                        <div className="d-flex flex-column">
                            <h6 className="mb-3 text-sm">Current Configuration</h6>
                            <span className="mb-2 text-xs">Pattern:
                                <span className="text-danger font-weight-bold ms-sm-2">{pattern.Value} {pattern.Mode} {pattern.Constrain} {pattern.Time} {pattern.Date}</span>
                            </span>
                            <span className="mb-2 text-xs">Next Occurance:
                                <span className="text-dark ms-sm-2 font-weight-bold">{nextOccurance}</span></span>
                        </div>
                        <div className="ms-auto text-end">
                            <a className="btn btn-link text-primary px-3 mb-0" href="javascript:;"><i className="fas fa-pencil-alt text-dark me-2" aria-hidden="true" />View Calender</a>
                        </div>
                    </li>
                </ul>
                <Calender name="Eat Eggs" dates={upComingDates} />
            </>
        );
    }

    const renderDaySelector = () => {
        if (patternMode == 1) {
            return (
                <>
                    {renderRepeatPattern()}
                    <hr className="horizontal dark mt-3" />
                    <label>Start From</label>
                    <div className="form-check">
                        <div className="row">
                            <div className="col-md-12">
                                <DatePicker value={startDay} onChange={setStartDay} />
                            </div>
                        </div>
                    </div>
                </>
            );
        }
        else if (patternMode == 2) {
            return (
                <>
                    <div className="col-md-12 mt-3">
                        <DaySelector data={days} onDataUpdate={(e) => setDays(e)} />
                    </div>
                    <>
                        {renderRepeatPattern()}
                        <hr className="horizontal dark mt-3" />
                        <label>Start From</label>
                        <div className="form-check">
                            <div className="row">
                                <div className="col-md-12">
                                    <DatePicker value={startDay} onChange={setStartDay} />
                                </div>
                            </div>
                        </div>
                    </>
                </>
            );
        }
        else if (patternMode == 3) {
            return (
                <div className="col-12 col-sm-12">
                    <label>Constrain By</label>
                    <div className="form-check">
                        <input checked={constrain === 1} className="form-check-input" type="radio" name="constrainBy" id="radDayOfMonth" onChange={() => setConstrain(1)} />
                        <label className="custom-control-label" htmlFor="radDayOfMonth">Day of Month</label>
                        <div className="row">
                            <div className="col-md-4">
                                <DayDropdown onSelection={(e) => setDayOfMonth(e)} value={dayOfMonth} />
                            </div>
                        </div>
                    </div>
                    <div className="form-check">
                        <input checked={constrain === 2} className="form-check-input" type="radio" name="constrainBy" id="radLogicaly" onChange={() => setConstrain(2)} />
                        <label className="custom-control-label" htmlFor="radLogicaly">Logicaly</label>
                        <div className="row">
                            <div className="col-md-4">
                                <WeekDropdown onSelection={(e) => setLogicalStart(e)} value={logicalStart} />
                            </div>
                            <div className="col-md-4">
                                <DayNameDropdown onSelection={(e) => setLogicalDay(e)} value={logicalDay} />
                            </div>
                        </div>
                    </div>
                    {renderRepeatPattern()}
                    <hr className="horizontal dark mt-3" />
                    <label>Start From</label>
                    <div className="form-check">
                        <div className="row">
                            <div className="col-md-12">
                                <DatePicker value={startDay} onChange={setStartDay} />
                            </div>
                        </div>
                    </div>
                </div>
            );
        }
        else if (patternMode == 4) {
            return (
                <>
                    {renderRepeatPattern()}
                    <hr className="horizontal dark mt-3" />
                    <label>Start From</label>
                    <div className="form-check">
                        <div className="row">
                            <div className="col-md-12">
                                <DatePicker value={startDay} onChange={setStartDay} />
                            </div>
                        </div>
                    </div>
                </>
            );
        }
    }



    return (
        <>
            <div className="row">
                {/*COURSE*/ }
                <div className="col-12 col-sm-12">
                    <label>Course</label>
                    <div className="col-12 col-sm-12">
                        <div className="form-check">
                            <input checked={course === 1} className="form-check-input" type="radio" name="course" id="radOneTime" onChange={() => setCourse(1)} />
                            <label className="custom-control-label" htmlFor="radOneTime">One Time</label>
                        </div>
                        <div className="form-check">
                            <input checked={course === 2} className="form-check-input" type="radio" name="course" id="radMultipleTimes" onChange={() => setCourse(2)} />
                            <label className="custom-control-label" htmlFor="radMultipleTimes">Multiple Times</label>
                        </div>
                    </div>
                </div>
                {/*PATTERN*/}
                <label className="custom-control-label mt-3" htmlFor="customRadio1">Running Pattern</label>
                <div className="col-md-1">
                    <label className="form-check-label" htmlFor="flexSwitchCheckDefault">Every</label>
                </div>
                <div className="col-md-2">
                    <input className="form-control" type="number" value={every} onChange={e => setEvery(Number(e.target.value))} />
                </div>
                <div className="col-md-4">
                    <select className="form-control" id="everyCount" onChange={e=>setPatternMode(Number(e.target.value))}>
                        <option value="1">Day</option>
                        <option value="2">Week</option>
                        <option value="3">Month</option>
                        <option value="4">Year</option>
                    </select>
                </div>

                {renderDaySelector()}

                {/*TIME*/}
                <label className="mt-3">Set Time</label>
                <div className="form-check">
                    <div className="row">
                        <div className="col-12 col-sm-12">
                            <div className="form-check">
                                <input checked={timeMode === 1} className="form-check-input" type="radio" name="time" id="radAdapt" onChange={() => setTimeMode(1)} />
                                <label className="custom-control-label" htmlFor="radAdapt">Adapt Automaticaly</label>
                            </div>
                            <div className="form-check">
                                <input checked={timeMode === 2} className="form-check-input" type="radio" name="time" id="radSpecificDate" onChange={() => setTimeMode(2)} />
                                <label className="custom-control-label" htmlFor="radSpecificDate">Specific Time</label>
                                {timeMode === 2 && <TimePicker value={startTime} onChange={setStartTime} />}
                            </div>
                        </div>
                    </div>
                </div>

            </div >

            <div className="card-footer pt-0 p-3 d-flex align-items-center">
                <div className="w-60"> <p className="text-sm"> </p>
                </div>
                <div className="w-40 text-end">
                    <a className="btn bg-gradient-primary mb-0 text-end" onClick={SaveData}>Save Details</a>
                </div>
            </div>
        </>
    );

}

This is the Calender component

export const Calender = ({name, dates}) => {

    const [upcomingDates, setUpcomingDates] = useState([])

    useEffect(() =>
    {
        let dts = [];
        for (let i = 0; i < dates.length; i++) {
            dts.push(
                { title: name, date: dates[i], className: 'bg-gradient-dark' }
            );
        }
        setUpcomingDates(dts);
    }, []);

    return (
        <div className="card card-calendar">
            <div className="card-body p-3">
                <FullCalendar
                    allDayClassNames="calendar"
                    plugins={[dayGridPlugin]}
                    initialView="dayGridMonth"
                    weekends={false}
                    events={upcomingDates}
                />
            </div>
        </div>
    ); }

Solution

  • I found the issue.

    Instead of using "upcomingDates" as dependency to the useEffect() in parent component

    I had to put it inside the child component (Calender component), So that it will rerender when dates changes

    export const Calender = ({name, dates}) => {
    
        const [upcomingDates, setUpcomingDates] = useState([])
    
        useEffect(() =>
        {
            let dts = [];
            for (let i = 0; i < dates.length; i++) {
                dts.push(
                    { title: name, date: dates[i], className: 'bg-gradient-dark' }
                );
            }
            setUpcomingDates(dts);
        }, [dates]);
    
        return (
            <div className="card card-calendar">
                <div className="card-body p-3">
                    <FullCalendar
                        allDayClassNames="calendar"
                        plugins={[dayGridPlugin]}
                        initialView="dayGridMonth"
                        weekends={false}
                        events={upcomingDates}
                    />
                </div>
            </div>
        );
    }