0

I've been banging my head against my desk for the past hour trying to figure out why the following code only displays the loading spinner and never updates to show the actual data even when I can see the data logged in the console, so I know the data is actually being fetched.

What is supposed to happen is that the page will display a spinner loader the first time the data is retrieved and then after useSWR has finished getting the data, the page will re-render to show the data. The data will eventually be shown in like a globe thing, but just as a prototype, I'm rendering it just with .map. The page is also not supposed to show the spinner a second time.

I think my problem might have something to do with calling the function to update the hook at the very end causes the whole page to re-render, although I'm not sure and if that is the case, I couldn't figure out how I'd make that work considering how SWR spontaneously re-fetches the data.

Here's the primary file:

import { useEffect, useState } from "react";
import Navbar from "@components/navbar";
import Globe from "@/components/interest-globes/globe";

import usePercentages from "@/components/interest-globes/globe-percentages";
const globeName = "primary";

const App = () => {
  const [globePercentages, setGlobePercentages] = useState([]);
  const [isFirstLoad, setFirstLoad] = useState(false);
  const [isLoading, setLoading] = useState(false);
  const [isError, setError] = useState();

  useEffect(() => {
    setFirstLoad(true);
  }, []);

  let {
    globePercentages: newGlobePercentages,
    isLoading: newLoadingState,
    isError: newErrorState,
  } = usePercentages(globeName);

  console.log(newGlobePercentages, newLoadingState, newErrorState);

  useEffect(() => {
    updateGlobe();
  }, [newGlobePercentages, newLoadingState, newErrorState]);

  const updateGlobe = () => {
    if (
      isFirstLoad &&
      (newGlobePercentages !== null || newGlobePercentages !== "undefined")
    ) {
      setFirstLoad(false);
    } else if (newLoadingState || !newGlobePercentages) {
      setLoading(true);
      return;
    } else if (newErrorState) {
      setError(newErrorState);
      return;
    }

    setGlobePercentages(newGlobePercentages);
  };

  return (
    <div>
      <Navbar />
      <div className="container-md">
        <h1>The percentages are:</h1>
        <br />
        <Globe
          globePercentages={globePercentages}
          isLoading={isLoading}
          isError={isError}
        />
        {/* <Globe props={{ globePercentages, isLoading, isError }} /> */}
        <br />
      </div>
    </div>
  );
};

export default App;

Here is the globe component:

import Loading from "@components/loading";
import Error from "@components/error";

const Globe = ({ globePercentages, isLoading, isError }) => {
// const Globe = ({ props }) => {
//   let { globePercentages, isLoading, isError } = props;

  if (isLoading) {
    return <Loading/>
  }
  else if (isError) {
    return <Error/>
  }

  return (
    <div>
      {globePercentages.map((interest) => (
        <div key={interest.name}>
          <h2>Name: {interest.name}</h2>
          <h4>Globe Percentage: {interest.globePercentage}%</h4>
          <br />
        </div>
      ))}
    </div>
  );
};

export default Globe;

Here is use-percentages:

import { useEffect } from "react";
import useSWR from "swr";
import fetcher from "@components/fetcher";

const usePercentages = (globeName) => {
  const url = `/api/v1/interest-globes/${globeName}/get-globe-percentages`;
  let { data, error } = useSWR(url, fetcher, { refreshInterval: 1000 });

  return {
    globePercentages: data,
    isLoading: !error && !data,
    isError: error,
  };
};

export default usePercentages;
thowitz
  • 38
  • 3

1 Answers1

1

If you move your hook into your Globe component, everything becomes so simple:

const App = () => {

  return (
    <div>
      <Navbar />
      <div className="container-md">
        <h1>The percentages are:</h1>
        <br />
        <Globe globeName={'primary'}/>
        <br />
      </div>
    </div>
  );
};

export default App;
const Globe = ({globeName}) => {

  const lastPercentages = useRef(null);

  const {
    globePercentages,
    isLoading,
    isError,
  } = usePercentages(globeName);

  // only show the loader if isLoading AND lastPercentages is false-y
  if (isLoading && !lastPercentages.current) {
    return <Loading/>
  }

  if (isError) {
    return <Error/>
  } 
  
  lastPercentages.current = globePercentages ?? lastPercentages.current;

  return (
    <div>
      {lastPercentages.current.map((interest) => (
        <div key={interest.name}>
          <h2>Name: {interest.name}</h2>
          <h4>Globe Percentage: {interest.globePercentage}%</h4>
          <br />
        </div>
      ))}
    </div>
  );  

}

The source of truth of globePercentages, isLoading, and isError come from the usePercentages hook. It feels so complicated because you're duplicating these pieces of data into new state variables, and then requiring that you keep them in sync. They are meant to be used as sources of truth, don't recreate them in your own state variables.

Adam Jenkins
  • 51,445
  • 11
  • 72
  • 100
  • The problem that I'm having is that I only want to display the loading spinner the first time the data is being fetched, but when fetching data, usePercentages returns twice. The first time it returns, globePercentages is undefined and isLoading is true and the second time it returns, globePercentages has the actual data. The globe component then needs to return, but if it returns globePercentages, 50% of the time, it'll return nothing and overwrite the current data, which I don't want to do unless I actually have the new data. – thowitz May 31 '21 at 20:36
  • What I really need is a way to return from the globe component without changing the data displayed. – thowitz May 31 '21 at 20:39
  • @thowitz - I have a feeling that `globePercentages` is null or undefined on the first fetch, but it won't be on subsequent fetches. **IF** that is correct, then `firstLoad` can be inferred in `globePercentages` is null or undefined. – Adam Jenkins May 31 '21 at 20:39
  • @thowitz - see my edit - only show the loader if `isLoading` is true AND `globePercentages` is falsey – Adam Jenkins May 31 '21 at 20:40
  • ```globePercentages``` is unfortunately null on every alternate subsequent returns – thowitz May 31 '21 at 20:52
  • @thowitz - thanks for the edit with the `.current` property. FYI - you don't need the last `elseif` - it's default. – Adam Jenkins May 31 '21 at 22:29
  • I think you do need the last else if because otherwise every time it's loading and globePercentages gets set to undefined, lastPercentages.current will then get set to undefined, whereas if you wait for only when globePercentages is equal to a proper value, then you'll get the functionality desired – thowitz Jun 02 '21 at 10:51
  • @thowitz nope, because there's an early return when it's loading. – Adam Jenkins Jun 02 '21 at 11:14
  • @thowitz - ah, you are correct, because of the funny logic, my bad, sorry! – Adam Jenkins Jun 04 '21 at 14:19