简体   繁体   中英

React JS proper way to set a loader UI

I was trying to replicate my local project within Code Sandbox and I found my mistake. What I am trying to do in these projects is show a loader spinner while fetching some data from a Express + GraphQL server, but instead, the loader won't hide when the data is loaded.

This is the code where I fetch the data:

import React, { useEffect, useContext, useState } from "react";
import GlobalContext from "../../context/Global/context";
import gql from "graphql-tag";
import { useLazyQuery } from "@apollo/react-hooks";

const GET_USERS = gql`
  query {
    users {
      id
      name
      address {
        street
      }
    }
  }
`;

export default props => {
  const globalContext = useContext(GlobalContext);
  const [getUsers, { called, loading, data: users }] = useLazyQuery(GET_USERS);

  useEffect(() => {
    getUsers();
  }, []);

  useEffect(() => {
    console.log(globalContext.loading)
    if (globalContext.loading && users.length) {
      globalContext.setLoading(false);
    }
  }, [globalContext, users]);

  if (called && loading) {
    globalContext.setLoading(true);
  }

  if (!users) {
    return <p>There are no users</p>;
  }
  console.log(users)

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Street</th>
        </tr>
      </thead>
      <tbody>
        {users.map(user => {
          return (
            <tr>
              <td>{user.id}</td>
              <td>{user.name}</td>
              <td>{user.address.street}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

And this is the code where I set up the Loader:

import React, { useContext, useEffect } from "react";
import { ApolloClient } from "apollo-boost";
import { ApolloProvider } from "@apollo/react-hooks";
import { HttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import Users from "./components/Users";
import GlobalContext from "./context/Global/context";
import Loader from "react-loader-spinner";

const cache = new InMemoryCache();
const link = new HttpLink({
  uri: "https://73rcp.sse.codesandbox.io/"
});

const client = new ApolloClient({
  link,
  cache
});

export default () => {
  const globalContext = useContext(GlobalContext);

  if (globalContext.loading) {
    return (
      <div
        style={{
          width: "100vw",
          height: "100vh",
          display: "flex",
          justifyContent: "center",
          alignItems: "center"
        }}
      >
        <Loader type="Puff" />
      </div>
    );
  }

  return (
    <ApolloProvider client={client}>
      <Users />
    </ApolloProvider>
  );
};

This is the frontend code

This is the backend code

I always have the same issue, not sure what I am doing wrong, but I guess it's because when I set globalContext.setLoading(true) the component rerenders and the first component App.js loads before than Users component.

If this is the error or not, what is the proper way to set a loader spinner while fetching any data from anywhere? Thanks in advance.

So after some debugging, the main problem was that Users component was unmounting before receiving the data from the server. Also, I encountered some naming conflicts.

// App.js
export default () => {
  const globalContext = useContext(GlobalContext);
  // This is making the Users component unmount which won't allow to change the
  // global loading state once the data is received, hence the perpetual loading.
  if (globalContext.loading) {
    return (
      <div
        style={{
          width: "100vw",
          height: "100vh",
          display: "flex",
          justifyContent: "center",
          alignItems: "center"
        }}
      >
        <Loader type="Puff" />
      </div>
    );
  }

  return (
    <ApolloProvider client={client}>
      <Users />
    </ApolloProvider>
  );
};

A simple fix would be to render both the Loader and the Users component, however, the former will be positioned absolutely so the user can't interact with the Users component.

export default () => {
  const globalContext = useContext(GlobalContext);

  return (
    <ApolloProvider client={client}>
      <Users />
      {globalContext.loading && (
        <div
          style={{
            width: "100vw",
            height: "100vh",
            display: "flex",
            position: "absolute",
            top: 0,
            backgroundColor: "white",
            justifyContent: "center",
            alignItems: "center"
          }}
        >
          <Loader type="Puff" />
        </div> 
      )}
    </ApolloProvider>
  );
};

On the Users component there were some issues:

// Users component
export default props => {
  const globalContext = useContext(GlobalContext);
  // data: users will not return users object, instead it is only renaming it. 
  // if we were to destructure it like data: { users }, an error would be thrown since 
  // data.users is undefined. 
  const [getUsers, { called, loading, data: users }] = useLazyQuery(GET_USERS);

  useEffect(() => {
    getUsers();
  }, [getUsers]);

  useEffect(() => {
    // data does not contain a length property since it is an object. Again an 
    // error would be thrown. 
    if (globalContext.loading && users.length) {
      globalContext.setLoading(false);
    }
  }, [globalContext, users]);

  if (called && loading) {
    globalContext.setLoading(true);
  }

  if (!users) {
    return <p>There are no users</p>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Street</th>
        </tr>
      </thead>
      <tbody>
        {/* Since data is not an array, map would be undefined (error again)*/}
        {users.map(user => {
          return (
            <tr>
              <td>{user.id}</td>
              <td>{user.name}</td>
              <td>{user.address.street}</td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

So to fix this we have:

export default props => {
  const globalContext = useContext(GlobalContext);
  const [getUsers, { called, loading, data }] = useLazyQuery(GET_USERS);

  useEffect(() => {
    getUsers();
  }, [getUsers]);

  useEffect(() => {
    if (globalContext.loading && data) {
      globalContext.setLoading(false);
    }
  }, [globalContext, data]);

  if (called && loading) {
    globalContext.setLoading(true);
  }

  if (!data) {
    return <p>There are no users</p>;
  }

  return (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Name</th>
          <th>Street</th>
        </tr>
      </thead>
      <tbody>
        {data &&
          data.users.map(user => {
            return (
              <tr>
                <td>{user.id}</td>
                <td>{user.name}</td>
                <td>{user.address.street}</td>
              </tr>
            );
          })}
      </tbody>
    </table>
  );
};

All changes are in this sandbox .

There are multiple ways to implement loader in react app while getting data or sending some data from/to server.

The best way is to create a higher order component that will load the wrapping feature component or a loader based on a Boolean flag.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM