Search code examples
reactjsinfinite-loopuse-effectusecallback

UseEffect and useCallback still causes infinite loop in react project


I can't seem to resolve an infinite loop issue in my react project.

I'm working on a daily-log react app. Let me explain the project briefly. Here is the picture of the code for quick view:

Here is the picture of the code for quick view

The same code is available at the bottom.

The structure (from top to bottom)

  1. The DailyLog component has a form that uses Question components into which props are passed.
  2. The Question component uses the props to display a question and description. It also contains an Input component into which props are further passed down.
  3. The Input component takes the props and renders the appropriate form input field.

The logic (from bottom to top)

  1. The Input component handles it's own inputState. The state is changed when the user inputs something and the onChangeHandler is triggered.
  2. The Input component also has a useEffect() hook that calls an onInput() function that was passed down as props from DailyLog.
  3. The onInputHandler() in the DailyLog component updates the formState which is the form-wide state containing all input field values. The formState is amended depending on which input field is filled at the time.
  4. The onInputHandler() uses the useCallback() hook which is supposed to stop an infinite loop caused by any parent/child re-renders. But it doesn't work :frowning:

What's wrong in the code? What am I missing here? Code provided below:

//DailyLog.js
import React, { useState, useCallback } from 'react';

import Question from '../components/FormElements/Question';
import questionData from '../components/DailyLog/questionData';
import './DailyLog.css';

const DailyLog = () => {
    const [formState, setFormState] = useState();

    const onInputHandler = useCallback(
        (inputId, inputValue) => {
            setFormState({
                ...formState,
                [inputId]: inputValue,
            });
        },
        [formState]
    );

    return (
        <main className="container">
            <form action="" className="form">
                <Question
                    id="title"
                    element="input"
                    type="text"
                    placeholder="Day, date, calendar scheme"
                    onInput={onInputHandler}
                />

                <Question
                    id="focus"
                    question={questionData.focusQuestion}
                    description={questionData.focusDescription}
                    element="textarea"
                    placeholder="This month's focus is... This week's focus is..."
                    onInput={onInputHandler}
                />
            </form>
        </main>
    );
};

export default DailyLog;

//Question.js
import React from 'react';

import Input from './Input';
import './Question.css';

const Question = props => {
    return (
        <div className="form__group">
            {props.question && (
                <label className="form__label">
                    <h2>{props.question}</h2>
                </label>
            )}

            <small className="form__description">{props.description}</small>

            <Input
                id={props.id}
                element={props.element}
                type={props.type}
                placeholder={props.placeholder}
                onInput={props.onInput}
            />
        </div>
    );
};

export default Question;

//Input.js
import React, { useState, useEffect } from 'react';

import './Input.css';

const Input = props => {
    const [inputState, setInputState] = useState();

    const { id, onInput } = props;

    useEffect(() => {
        onInput(id, inputState);
    }, [id, onInput, inputState]);

    const onChangeHandler = event => {
        setInputState(event.target.value);
    };

    // check if question element type is for input or textarea
    const element =
        props.element === 'input' ? (
            <input
                id={props.id}
                className="form__field"
                type={props.type}
                value={inputState}
                placeholder={props.placeholder}
                onChange={onChangeHandler}
            />
        ) : (
            <textarea
                id={props.id}
                className="form__field"
                rows="1"
                value={inputState}
                placeholder={props.placeholder}
                onChange={onChangeHandler}
            />
        );

    return <>{element}</>;
};

export default Input;


Solution

  • Remove id and onInput from useEffect sensivity list

    useEffect(() => {
            onInput(id, inputState);
    }, [inputState]);
    

    And set default value of inputState to '' as follow:

    const [inputState, setInputState] = useState('');
    

    To prevent 'A component is changing an uncontrolled input of type text to be controlled error in ReactJS'. Also you can init formState:

    const [formState, setFormState] = useState({title:'', focus:''});