Sahil Sharma
Sahil Sharma

Reputation: 1563

Chaining useEffects with early return

Example to make the context clear:

I am trying to render a component with two sets of data coming from API calls. I am also returning early if the first API call fails. The second API call depends on the data of the first API result. I don't want to combine both effects because that would mean the whole of component does not render till I get bot API results.

This is the psuedo code

const DataList = () => {
  const [dataFromEffect1, setDataFromEffect1] = useState([]);
  const [dataFromEffect2, setDataFromEffect2] = useState([]);

  useEffect(() => {
    const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
    callApi1();
  }, []);

  // early return so that all the complex logic below is not called on ever render
  if (!dataFromEffect1) return <div>No Data1</div>;

  const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect

  useEffect(() => {
    const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
    callApi2();
  }, [data1]);

  return (
    <div>
      {/* no need to null check here, because of the early return on top */}
      {dataFromEffect1}
      {/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
      {dataFromEffect2 ? (
        <div>
          {dataFromEffect2}
        </div>
      ) : null}
    </div>
  );
};

Problem

The above code does not work because you cannot add a useEffect conditionally (the early return messes it up) Trying to find the best workaround for this problem.

Upvotes: 0

Views: 436

Answers (3)

Sahil Sharma
Sahil Sharma

Reputation: 1563

Two options ( I went with the second one for now):

  1. create a separate component for abstracting the second useEffect as suggested by @viet in the answer below.
  2. move the useEffect above the early return, but I will have to duplicate the if conditions inside the effect. As described by in the answer below.

Upvotes: 0

Hans Krohn
Hans Krohn

Reputation: 433

You can try something like this. It will kill both useEffects, but it will not run the second one unless it retrieved data from the first. Also, I did not fix this in your code but you should not use async code within useEffect. This can lead to memory leaks and unnexpected bugs. You are also not cleaning up the fetch from within the useEffect. Academind has a nice blog explaining how to fix this and what will happen if you keep the code like this https://academind.com/tutorials/useeffect-abort-http-requests/

const DataList = () => {
  const [dataFromEffect1, setDataFromEffect1] = useState([]);
  const [dataFromEffect2, setDataFromEffect2] = useState([]);

  useEffect(() => {
    const callApi1 = async () => setDataFromEffect1(await (await fetch('/api1')).json());
    callApi1();
  }, []);

 useEffect(() => {
    if(!dataFromEffect1.length) return;
    const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect
    const callApi2 = async () => setDataFromEffect2(await (await fetch('/api2', { headers: data1 })).json());
    callApi2();
  }, [dataFromEffect1]);

  // early return so that all the complex logic below is not called on ever render
  if (!dataFromEffect1) return <div>No Data1</div>;


  return (
    <div>
      {/* no need to null check here, because of the early return on top */}
      {dataFromEffect1}
      {/* null check required here, so that it doesnt render this child component to not render till we get the data for it */}
      {dataFromEffect2 ? (
        <div>
          {dataFromEffect2}
        </div>
      ) : null}
    </div>
  );
};

Upvotes: 1

Viet
Viet

Reputation: 12807

Just creat a component for dataFromEffect2:

const DataList = () => {
  const [dataFromEffect1, setDataFromEffect1] = useState([]);
  const [dataFromEffect2, setDataFromEffect2] = useState([]);

  useEffect(() => {
    const callApi1 = async () =>
      setDataFromEffect1(await (await fetch("/api1")).json());
    callApi1();
  }, []);
  
  if (!dataFromEffect1) return <div>No Data1</div>;

  const data1 = complexMassagingOver(dataFromEffect1); // data1 to be used in second effect

  return (
    <div>
      {dataFromEffect1}
      <Component data1={data1} dataFromEffect2={dataFromEffect2} setDataFromEffect2={setDataFromEffect2} />
    </div>
  );
};

const Component = ({ data1, dataFromEffect2, setDataFromEffect2 }) => {
  useEffect(() => {
    const callApi2 = async () =>
      setDataFromEffect2(
        await (await fetch("/api2", { headers: data1 })).json()
      );
    callApi2();
  }, [data1]);

  return <div>{dataFromEffect2}</div>;
};

Upvotes: 1

Related Questions