Using the React-Async Library for Declarative Data Fetching

Introduction

When fetching data in a JavaScript application, async-await allows us to use imperative synchronous programming for fetching data. This means that our code will imperatively describe how the program will function and will operate along a single thread of operations. React, on the other hand, is a library that build UIs declaratively. This is often considered to be an opposing method to imperative programming, in which the developer describes what they want the program to do, rather than how they want it to behave. The combination of React and async-await therefore leads to a mixed pattern in which we have both imperative code (for data fetching) and declarative code (for UI composition).

React-async provides a declarative API to perform any REST API calls using a single React component, allowing declarative programming to be used throughout the application. It takes care of handling errors, promise resolution, and retrying promises, and deals with local asynchronous state.

In this article, we will explain how the React-Async library helps us fetch data and run through some demonstrations of Helper components and functions.

Fetching Data Declaratively with React-Async

Let’s break down our topic into two parts: Declarative data fetching and Asynchronous API Calls in React.

Declarative data fetching is an approach used in calling APIs in which you declare what you want it to do for you without you worrying about all the things related to the call. It’s the opposite of the imperative approach where you also need to detail the steps you want the program to take.

Since JavaScript is synchronous by default and is single threaded, when we want to render a component that shows some data coming from an asynchronous call in previous versions of React, classes needed to be used. We had to use the component lifecycle methods to ensure the call happened when the component was mounted, and then we used the local state to manage the loading state.

Asynchronous requests will wait for a request to respond while the rest of the code continues to execute. Then when the time is right, a callback will spring these asynchronous requests into action.

Let’s demonstrate this by making a call to an endpoint to grab a list of currency prices:

import React, { Component } from 'react'; import axios from 'axios';  class App extends Component {   state = {     data: [],     error: '',   };    componentDidMount() {          axios       .get('https://api.coinmarketcap.com/v1/ticker/?limit=1')       .then(res => this.setState({ data: res.data }))       .catch(error => this.setState({ error }));   }    render () {     return (       <div className="App">         <ul>           {this.state.data.map(el => (             <li>               {el.name}: {el.price_usd}             </li>           ))}         </ul>       </div>     );   } }  export default App; 

Note: Install axios by typing ‘npm install axios` into your terminal.

You can see this code live using this CodeSandbox page.

Here we make our API call in the componentDidMount function to ensure it runs as soon as the component is loaded. We can make sense of our data only after we go through the following steps:

  • First, we make a request to the API.
  • We then receive a response.
  • We extract data from the response.
  • Finally, we store the data in our local state.

In the event of an error during the data fetching process:

  • We catch the error.
  • We then store the data in our local state.

Here we are explicitly describing how and what to do at every step in fetching the data. Though the syntax and functions work correctly, the code can be re-written to be more efficient and in fewer lines if written declaratively. Let’s rewrite it using React-Async.

First, you need to install the package by typing npm install react-async in your terminal. Then write your component with the following code:

import React, { Component } from 'react'; import Async from 'react-async';  const loadJson = () =>   fetch("https://api.coinmarketcap.com/v1/ticker/?limit=1")     .then(res => (res.ok ? res : Promise.reject(res)))     .then(res => res.json())  const App = () => (   <Async promiseFn={loadJson}>     {({ data, error, isLoading }) => {       if (isLoading) return "Loading..."       if (error) return ``Something went wrong: ${error.message}``        if (data)         return (           <div>              {data.map(el => (               <li>                 {el.name}: {el.price_usd}               </li>              ))}           </div>         )        return null     }}   </Async> )  export default App; 

If you are using CodeSandbox, add React-Async from the dependency menu. If you’d like to see this code live, take a look at the React-Async example in CodeSandbox.

Here we have rewritten our component using hooks instead of classes. We first create a function loadJson to handle our data fetching. Then, inside our App component, we utilize the Async component made available through the React-Async library.

Once our promise is resolved, props are made available to us to handle different scenarios.

  • isLoading is available so we can display a user-friendly message while the data is yet to be loaded.
  • error is available in case of an error during the fetch.
  • data is the actual data returned after the fetching is complete.

In this example, we no longer have to use classes or lifecycle methods to load our data, nor do we need to tell React-Async how to process the data or how to update our state.

React-Async manages the loading state through the isLoading fallback prop, which is rendered until data is ready to be rendered, that is, when the dependent asynchronous call resolves and returns the data.

Helper Components

React-Async comes with several helper components that make your JSX more declarative and less cluttered. Each of the helper components will only render its children when appropriate. We can rewrite our App function to look like this:

const App = () => (   <Async promiseFn={loadJson}>     <Async.Loading>Loading...</Async.Loading>      <Async.Resolved>       {data => (         <div>                      {data.map(el => (             <li>               {el.name}: {el.price_usd}             </li>           ))}         </div>       )}     </Async.Resolved>      <Async.Rejected>       {error => `Something went wrong: ${error.message}`}     </Async.Rejected>   </Async> ) 

Check out this example on CodeSandbox.

In this example, we have used the Async.Loading, Async.Resolved, and Async.Rejected functions to simplify our code and make it more readable. Helper components provided by React-Async can take a React element or a function as children. When you provide a function, you’ll receive render props you can use in your component.

User Profile Test App with Helper Functions

Let’s build a small user profile app that uses some more helper functions. Update your component to the following code:

import React, { Component } from 'react'; import Async from 'react-async';  const loadUser = ({ userId }) =>   fetch('`https://reqres.in/api/users/${userId}'`)     .then(res => (res.ok ? res : Promise.reject(res)))     .then(res => res.json())  const UserPlaceholder = () => (   <div>     <div>User Details Loading</div>   </div> )  const UserDetails = ({ data }) => (   <div className="details">     <img className="avatar" src={data.data.avatar} alt="" />     <div>       {data.data.first_name} {data.data.last_name}     </div>   </div> )  const App = () => (     <Async promiseFn={loadUser} userId={1}>       <Async.Pending>         <UserPlaceholder />       </Async.Pending>       <Async.Fulfilled>{data => <UserDetails data={data} />}</Async.Fulfilled>       <Async.Rejected>{error => <p>{error.message}</p>}</Async.Rejected>     </Async> ) export default App; 

This is the code in CodeSandbox.

Let’s go over the functions we declared:

  • loadUser: We define this function to handle data fetching. It takes in a prop (userId) and queries the API based on the id.
  • userPlaceholder: This is the fallback component that will be displayed when the promise has not yet resolved, that is, when the data has not finished loading.
  • userDetails: This component handles the actual display of the user data. It takes the data in via props and is only rendered when the promise has been resolved.
  • Async.Pending,Async.Fulfilled, and Async.Rejected: These are functions to simplify our code and make it more readable.

Conclusion

In this tutorial, we explored how to use the React-Async library to help us fetch data declaratively. We also looked at some of the helper functions it offers. To learn more about React-Async, check out the React-Async docs on GitHub.