0

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>
    ); }
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Sangeeth Nandakumar
  • 1,362
  • 1
  • 12
  • 23

2 Answers2

1

Here's everything that is happening and why it is not working:

One, when you add the state dependency to the useEffect it falls into and infinite loop because the function it is calling modifies that state.

The component is definitely rerendering when the state changes, and the effect is acting as you would expect, a useEffect with an empty dependency array is called only on mount and dismount.

If it is rerendering with an empty array, it is very likely because pattern.UpcomingDates is an empty array, not because the component isn't acting as expected. Have you tried logging what pattern.UpcomingDates is?

Past that, there are some anti-pattern stuff going on in your component that other people have made note of. You should either declare that function inside of the effect, or create it use useCallback and add it to the dependency array of the useEffect.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Cory Harper
  • 1,023
  • 6
  • 14
  • Thanks mate. I understood the cause of infinite loop. My concern is why the component not retendering it's child components even after I call 'setUpcommingDates()'. Becaue my other parts are properly changing with change in state – Sangeeth Nandakumar Sep 14 '21 at 21:48
  • Here's a coiple of things to try. One, take away the keyword `async` in your useEffect, you can not pass an async callback to `useEffect` anyway and you are not awaiting anything. After that, can you do something like `useEffect(() => console.log(upcomingDates), [upcomingDates])` and give me some idea of the results? – Cory Harper Sep 14 '21 at 22:12
  • ' anyway and you are not awaiting anything' - I'm awaiting an API call in the useEffect(). Should I move the call to a different function? – Sangeeth Nandakumar Sep 15 '21 at 06:27
  • You weren't awaiting it in your example, you are just calling the asynchronous function. – Cory Harper Sep 15 '21 at 15:18
0

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>
    );
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Sangeeth Nandakumar
  • 1,362
  • 1
  • 12
  • 23