useEffect dependencies not re-rendering component

Viewed 947

I'm trying to re-render a component using useEffect and passing dependencies, but it isn't working. I'm quite new to react hooks so I think I might not be passing the correct dependency. I'm fetching some info and updating the state, however, when I passed the dependencies the re-render cycle does not happen.

Here is the code:

import React, { useRef, useState, useEffect } from "react";

import Loader from "../UI/loader/loader";

import axios from "axios";
import "./surveybox.css";

interface surveryAnswer {
  id: number;
  answers: string[];
}

const SurveyBox: React.FC = () => {
  const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
  const [loading, setLoading] = useState(false);
  const [numberOfAnswers, setNumberOfAnswers] = useState<number>();

  const programmingQRef = useRef<HTMLSelectElement>(null);
  const skillsQRef = useRef<HTMLSelectElement>(null);
  const stateManagementQRef = useRef<HTMLSelectElement>(null);
  const programmerTypeQRef = useRef<HTMLSelectElement>(null);

  useEffect(() => {
    axios
      .get(`${DB_URL}/users-answers.json`)
      .then((res) => {
        const fetchedAnswers = [];
        for (let item in res.data) {
          fetchedAnswers.push({
            ...res.data[item],
            id: item,
          });
        }
        setNumberOfAnswers(fetchedAnswers.length);
      })
      .catch((err) => {});
  }, [numberOfAnswers, setNumberOfAnswers]);

  const onSubmitSurvey = (e: React.FormEvent): void => {
    e.preventDefault();
    setLoading((prevLoading) => !prevLoading);
    const newAnswer = {
      id: Math.random(),
      answers: [
        programmerTypeQRef.current!.value,
        skillsQRef.current!.value,
        stateManagementQRef.current!.value,
        programmerTypeQRef.current!.value,
      ],
    };

    setSurveyUserAnswers(newAnswer);

    axios
      .post(`${DB_URL}/users-answers.json`, newAnswer)
      .then((res) => {
        setLoading((prevLoading) => !prevLoading);
      })
      .catch((error) => {
        console.log(error);
        setLoading((prevLoading) => !prevLoading);
      });
  };

  return (
    <div className="surveybox-container">
      {loading ? (
        <div className={"loader-holder"}>
          <Loader />
        </div>
      ) : (
        <React.Fragment>
          <h2>Quick survey!</h2>
          <form action="submit" onSubmit={onSubmitSurvey}>
            <label>favorite programming framework?</label>
            <select ref={programmingQRef} name="programming">
              <option value="React">React</option>
              <option value="Vue">Vue</option>
              <option value="Angular">Angular</option>
              <option value="None of the above">None of the above</option>
            </select>
            <br></br>
            <label>what a junior developer should have?</label>
            <select ref={skillsQRef} name="skills">
              <option value="Eagerness to lear">Eagerness to learn</option>
              <option value="CS Degree">CS Degree</option>
              <option value="Commercial experience">
                Commercial experience
              </option>
              <option value="Portfolio">Portfolio</option>
            </select>
            <br></br>
            <label>Redux or Context Api?</label>
            <select ref={stateManagementQRef} name="state-management">
              <option value="Redux">Redux</option>
              <option value="Context Api">Context Api</option>
            </select>
            <br></br>
            <label>Backend, Frontend, Mobile?</label>
            <select ref={programmerTypeQRef} name="profession">
              <option value="Back-end">back-end</option>
              <option value="Front-end">front-end</option>
              <option value="mobile">mobile</option>
            </select>
            <br></br>
            <button type="submit">submit</button>
          </form>
          <p>answered by {numberOfAnswers} visitors</p>
        </React.Fragment>
      )}
    </div>
  );
};

I'm passing the two dependencies that are involved in the useEffect and changes but still not working.

Thank you!

Edited Solution after recommendation:

...
interface surveryAnswer {
  id: number;
  answers: string[];
}

const SurveyBox: React.FC = () => {
  const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
  const [loading, setLoading] = useState(false);
  const [numberOfAnswers, setNumberOfAnswers] = useState<number>(0);

  const programmingQRef = useRef<HTMLSelectElement>(null);
  const skillsQRef = useRef<HTMLSelectElement>(null);
  const stateManagementQRef = useRef<HTMLSelectElement>(null);
  const programmerTypeQRef = useRef<HTMLSelectElement>(null);

  const fetchAnswersFromServer = () => {
    axios
      .get(`${DB_URL}/users-answers.json`)
      .then((res) => {
        const fetchedAnswers = [];
        for (let item in res.data) {
          fetchedAnswers.push({
            ...res.data[item],
            id: item,
          });
        }
        setNumberOfAnswers(fetchedAnswers.length);
      })
      .catch((err) => {});
  };

  const onSubmitSurvey = (e: React.FormEvent): void => {
    e.preventDefault();
    setLoading((prevLoading) => !prevLoading);
    const newAnswer = {
      id: Math.random(),
      answers: [
        programmerTypeQRef.current!.value,
        skillsQRef.current!.value,
        stateManagementQRef.current!.value,
        programmerTypeQRef.current!.value,
      ],
    };

    setSurveyUserAnswers(newAnswer);

    axios
      .post(`${DB_URL}/users-answers.json`, newAnswer)
      .then((res) => {
        fetchAnswersFromServer();
        setLoading((prevLoading) => !prevLoading);
      })
      .catch((error) => {
        console.log(error);
        setLoading((prevLoading) => !prevLoading);
      });
  };

  return (...
2 Answers

You probably only want the HTTP call to be made once (when the component mounts), so use [] as the dependency list. The HTTP call doesn't depend on any other props or state, so this most likely what you want.

If you want it to run more often than that, then you'll need to decide what data changes that requires you to make the HTTP call again, and add that to the dependency list instead of numberOfAnswers or setNumberOfAnswers.

The problem is that your effect lists numberOfAnswers as a dependency, and it also updates numberOfAnswers when it runs. So, this is likely what's happening:

  • The component mounts. numberOfAnswers is undefined by default.
  • The effect runs for the first time.
  • The HTTP request returns, say with 10 answers. It calls setNumberOfAnswers(10)
  • The component rerenders with numberOfAnswers=10
  • The effect runs again, because numberOfAnswers changed.
  • The HTTP request returns, probably with the same 10 answers. It calls setNumberOfAnswers(10)
  • The component does not rerender again because the value hasn't changed.

Issue

numberOfAnswers doesn't appear to be a dependency as it's not referenced in the effect callback, and secondly, unconditionally calling setNumberOfAnswers to update the numberOfAnswers can likely create an infinite loop. I suspect this effect runs once on mount, updates the state and runs a second time, and updates state again to the same value and then never updates again since the state is the same value.

Solution

So I was thinking to re-render every time there is a new submission.

You can trigger the useEffect on the surveyUserAnswers being updated.

Additional suggestions:

  1. No need to create an new array and push elements into it just to compute the number of answers. Use the res.data.length if an array, or Object.values(res.data).length if it's an object.
  2. Don't toggle the loading state, just set to true when starting the asynchronous loading action, and use a finally block to clear the loading state regardless if the Promise resolved or rejected.

Code:

const SurveyBox: React.FC = () => {
  const [surveyUserAnswers, setSurveyUserAnswers] = useState<surveryAnswer>();
  const [loading, setLoading] = useState(false);
  const [numberOfAnswers, setNumberOfAnswers] = useState<number>();

  const programmingQRef = useRef<HTMLSelectElement>(null);
  const skillsQRef = useRef<HTMLSelectElement>(null);
  const stateManagementQRef = useRef<HTMLSelectElement>(null);
  const programmerTypeQRef = useRef<HTMLSelectElement>(null);

  useEffect(() => {
    axios
      .get(`${DB_URL}/users-answers.json`)
      .then((res) => {
        setNumberOfAnswers(res.data.length); // or Object.values(res.data).length
      })
      .catch((err) => {});
  }, [surveyUserAnswers]);

  const onSubmitSurvey = (e: React.FormEvent): void => {
    e.preventDefault();
    setLoading(true);
    const newAnswer = {
      id: Math.random(),
      answers: [
        programmerTypeQRef.current!.value,
        skillsQRef.current!.value,
        stateManagementQRef.current!.value,
        programmerTypeQRef.current!.value,
      ],
    };

    setSurveyUserAnswers(newAnswer);

    axios
      .post(`${DB_URL}/users-answers.json`, newAnswer)
      .then((res) => {
        // handle any processing
      })
      .catch((error) => {
        console.log(error);
        // handle any other error processing
      })
      .finally(() => setLoading(false));
  };

Alternative Solution

Define onSubmitSurvey async and handle all the asynchronous logic via awaits and completely remove the useEffect hook.

  const onSubmitSurvey = async (e: React.FormEvent): void => {
    e.preventDefault();
    setLoading(true);
    const newAnswer = {
      id: Math.random(),
      answers: [
        programmerTypeQRef.current!.value,
        skillsQRef.current!.value,
        stateManagementQRef.current!.value,
        programmerTypeQRef.current!.value,
      ],
    };

    setSurveyUserAnswers(newAnswer);

    try {
      await axios.post(`${DB_URL}/users-answers.json`, newAnswer);
      const res = await axios.get(`${DB_URL}/users-answers.json`);
      setNumberOfAnswers(res.data.length); // or Object.values(res.data).length
    } catch(error) {
      console.log(error);
      // handle any other error processing
    } finally {
      setLoading(false);
    }
  };
Related