Standard way of fetching external data before showing webpage (2021 September)

Viewed 144

I like the way in AngularJS of fetching external data before showing webpage. The data will be sent one by one to the frontend before showing the webpage. We are certain that the website and the data on it is good when we see it.

$stateProvider
  .state('kpi', {
      url:'/kpi',
      templateUrl: '/htmls/kpi.html',
      resolve: {
          getUser: ['lazy', 'auth', function (lazy, auth) { return auth.getUser() }],
          postPromise: ['posts', 'getUser', function (posts, getUser) { return posts.getAll() }],
          userPromise: ['users', 'postPromise', function (users, postPromise) { return users.getAll() }],
          logs: ['kpiService', 'userPromise', function (kpiService, userPromise) { return kpiService.getLogs() }],
          subscribers: ['kpiService', 'logs', function (kpiService, logs) { return kpiService.getSubscribers() }]
      },
      controller: 'KpiCtrl'
})

Now, I would like to achieve this in ReactJS, I tried:

class Kpi extends React.Component {
  state = { logs: [] };

  getChartOptions1() {
    // this.state.logs is used
  }

  componentDidMount() {
      axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
          logs => {
            this.setState({ logs.data });
          });
  };

  render() {
    return;
    <div>
      <HighchartsReact
        highcharts={Highcharts}
        options={this.getChartOptions1()}
        {...this.props}
      />
      <div>{JSON.stringify(this.state.logs)}</div>
    </div>;
  }
}

But it seems that it first called getChartOptions1 with unready data, rendered the webpage, then fetched the external data, then called again getChartOptions1 with ready data, rendered the webpage again.

I don't like the fact that getChartOptions was called twice (first with unready data), and the page was rendered twice.

There are several ways discussed: Hooks, React.Suspense, React.Lazy, etc. Does anyone know what's the standard way of fetching external data before showing the webpage in React?

3 Answers

As suggested in comments, conditional rendering might look like

class Kpi extends React.Component {
    state = { logs: [] };

    getChartOptions1 () {
       // this.state.logs is used
    }

    componentDidMount() {
        axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
            logs => {
              this.setState({logs.data});
            });
    };

    render() {
        return this.state.logs.length ?
          (
            <div>
                <HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
                <div>{JSON.stringify(this.state.logs)}</div>
            </div>
          )
          : (<div>'Loading'</div>);
    }
}

but it might be better to start with logs: null, in case the fetch returns an empty array

class Kpi extends React.Component {
    state = { logs: null };

    getChartOptions1 () {
       // this.state.logs is used
    }

    componentDidMount() {
        axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
            logs => {
              this.setState({logs.data});
            });
    };

    render() {
        return this.state.logs ?
          (
            <div>
                <HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
                <div>{JSON.stringify(this.state.logs)}</div>
            </div>
          )
          : (<div>'Loading'</div>);
    }
}

Like mentioned above, the answer to your problem is is conditional rendering in the Kpi component. But you can take it one step further and make the conditional render directly on the chartoptions since they are the ones you need at the end.

You could also write Kpi as a functional component and go for a hook-solution. And by doing so de-coupling the data fetching from your Kpi component.

You can also make the hook more generic to handle different endpoints so the hook can be reused in multiple places in your app.

export const useBackendData = (initialEndpoint) => {
 const [endpoint, setEndpoint] = useState(initialEndpoint);
 const [data, setData] = useState([]);

  useEffect(() => {
  const fetchData = async () => {
    try {
     const response = await axios.get(endpoint);
     setData(response.data);
    }catch(e){
      // error handling...
    }
  }
  fetchData();
  }, [endpoint])

  return [data, setEndpoint]
}

const Kpi = (props) => {
  [chartOptions, setChartOptions] = useState(null);
  [logs] = useBackendData(`${BACKEND_URL}/httpOnly/kpi/logs`);

  const getChartOptions1 = () => {
    // do stuff with logs...
    setChartOptions([PROCESSED LOG DATA]);
  }

  useEffect(() => {
   if(!!logs.length)
     setChartOptions(getChartOptions1())        
  },[logs]);
  
  return !!chartOptions ? (
     <div>
        <HighchartsReact highcharts={Highcharts} options={chartOptions} {...props} />
        <div>{JSON.stringify(logs)}</div>
     </div>) : (
     <div>Loading data...</div>
     );
    }

React lifecycle is like this - first, the render method and then the componentDidMount is called (as per your case) during mounting of components, so you are having this issue.

What I do is show a loader, spinner(anything) till the data is being fetched, and once the data is there, show the actual component. The app needs to do something till it gets the data.

Related