Asynchronous data management has always been a focus and difficulty in the front end. It can be said that 80% of Web applications will have asynchronous data requests and consumption in the UI, and processing asynchronous data is its core business logic in a considerable number of Web applications.

In the React ecosystem, most people use state management to maintain asynchronous data, such as using Redux and asynchronous actions to retrieve remote data and store it in the Store.

But at this point in time, 9012, I don’t think it’s an elegant way to maintain asynchronous data using state management. With the emergence of React Hooks, I think it makes more sense to maintain asynchronous data directly within components. It is better than using state management, both in terms of development efficiency and maintainability.

Why do you say that? Let’s look at it in code.

Now, suppose we want to implement a function that takes a TodoList data and renders it with a component.

The simplest is to use the lifecycle directly within the component to fetch the data and then store it in state within the component.

Use the React lifecycle

import React from 'react'

class Todos extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      loading: false.todos: [].error: null,}}async componentDidMount() {
    this.setState({ loading: true })
    try {
      const todos = await (await fetch(
        'https://jsonplaceholder.typicode.com/todos',
      )).json()
      this.setState({ todos, loading: false})}catch (error) {
      this.setState({ error, loading: false })
    }
  }

  render() {
    const { loading, todos, error } = this.state

    if (loading) return <span>loading...</span>
    if (error) return <span>error!</span>
    return (
      <ul>
        {todos.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>)}}Copy the code

See online Demo:

This approach is very, very intuitive, but the biggest problem is that the asynchronous data cannot be changed externally, and the data cannot be changed after the component is rendered. This is why most people use state management to maintain asynchronous data.

Let’s take a look at how to maintain asynchronous data using Redux.

Use Redux

Assuming we have used the Redux middleware redux-Thunk, we would have code similar to the following:

First, we’ll define the string definition as a constant to a constant.js

export const LOADING_TODOS = 'LOADING_TODOS'
export const LOAD_TODOS_SUCCESS = 'LOAD_TODOS_SUCCESS'
export const LOAD_TODOS_ERROR = 'LOAD_TODOS_ERROR'
Copy the code

Next, write asynchronous actions, actions.js:

import {
  LOADING_TODOS,
  LOAD_TODOS_SUCCESS,
  LOAD_TODOS_ERROR,
} from '.. /constant'

export function fetchTodos() {
  return dispatch= > {
    dispatch({ type: LOADING_TODOS })
    return fetch('https://jsonplaceholder.typicode.com/todo')
      .then(response= > response.json())
      .then(todos= > {
        dispatch({
          type: LOAD_TODOS_SUCCESS,
          todos,
        })
      })
      .catch(error= > {
        dispatch({
          type: LOAD_TODOS_ERROR,
          error,
        })
      })
  }
}
Copy the code

Next, the data, todos.js, was processed in the Reducer

import {
  LOADING_TODOS,
  LOAD_TODOS_SUCCESS,
  LOAD_TODOS_ERROR,
} from '.. /constant'

const initialState = {
  loading: false.data: [].error: null,}export default function(state = initialState, action) {
  switch (action.type) {
    case LOADING_TODOS:
      return {
        ...state,
        loading: true,}case LOAD_TODOS_SUCCESS:
      return {
        ...state,
        data: action.todos,
        loading: false,}case LOAD_TODOS_ERROR:
      return {
        ...state,
        error: action.error,
        loading: false,}default:
      return state
  }
}
Copy the code

That’s not all. Finally, use it in components:

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { fetchTodos } from '.. /actions'

class Todos extends Component {
  componentDidMount() {
    const { dispatch } = this.props
    dispatch(fetchTodos)
  }

  render() {
    const { loading, items, error } = this.props
    if (loading) return <span>loading...</span>
    if (error) return <span>error!</span>

    return (
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>)}}const mapStateToProps = state= > {
  const { todos } = state
  return {
    loading: todos.loading,
    items: todos.data,
    error: todos.error,
  }
}

export default connect(mapStateToProps)(Todos)
Copy the code

See online Demo:

We can see that using Redux to manage asynchronous data leads to a huge amount of code, a lot of redundancy, a lot of template code, and in my opinion, a solution like Redux is not elegant in terms of development efficiency, development experience, and maintainability and readability.

Let’s look at how to get asynchronous data using React Hooks.

Use the React Hooks

We use a library called Dahlia-rest useFetch to fetch data. We can easily get data state {loading, data, error} and render it:

import React from 'react'
import { useFetch } from 'dahlia-rest'

const Todos = (a)= > {
  const { loading, data, error } = useFetch(
    'https://jsonplaceholder.typicode.com/todos'.)if (loading) return <span>loading...</span>
  if (error) return <span>error!</span>

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>)}export defulat Todos
Copy the code

The full use of dahlia-rest can be seen in dahlia-rest

See online Demo:

The code is very concise and the loading state and error handling are very elegant. As you may have noticed, there is no external way to change the state of the data using the lifecycle.

Hooks updates asynchronous data

Maintain asynchronous data using hooks. There are three ways to update asynchronous data, using dahlia-rest for example.

Internal refetch

This is the simplest way to retrieve the data and is usually used if the action that triggered the update and useFetch are within a unified component.

const Todos = (a)= > {
  const { loading, data, error, refetch } = useFetch('/todos', {
    query: { _start: 0._limit: 5 }, // first page
  })

  if (loading) return <span>loading...</span>
  if (error) return <span>error!</span>

  const getSecondPage = (a)= > {
    refetch({
      query: { _start: 5._limit: 5 }, // second page})}return (
    <div>
      <button onClick={getSecondPage}>Second Page</button>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>)}Copy the code

See online Demo:

Update dependencies (DEPS)

Retrieving data by updating dependencies is also a common way to do this, because in many business scenarios the triggering action is in other components. Here is how to trigger data updates by updating dependencies:

A simple state management library is used to maintain dependent objects. See Dahlia-store for the full documentation of state management.

Define a store to store dependencies:

// /stores/todoStore.ts
import { createStore } from 'dahlia-store'

const todoStore = createStore({
  params: {
    _start: 0._limit: 5,
  },
  updateParams(params) {
    todoStore.params = params
  },
})
Copy the code

In a component, use dependencies:

import { observe } from 'dahlia-store'
import todoStore from '@stores/todoStore'

const Todos = observe((a)= > {
  const { params } = todoStore
  const { loading, data, error } = useFetch('/todos', {
    query: params,
    deps: [params],
  })

  if (loading) return <span>loading...</span>
  if (error) return <span>error!</span>

  const updatePage = (a)= > {
    todoStore.updateParams({ _start: 5._limit: 5})}return (
    <div>
      <button onClick={updatePage}>Update Page</button>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>)})Copy the code

See online Demo:

You can update dependencies anywhere, inside or outside a component, by calling todoStore.updateParams.

Note: the dependency here is an object, you must update the reference to the entire object, if you update only the attributes of the object is not valid.

Using a fetcher

Sometimes you need to retrieve data outside of the component, but useFetch doesn’t have any parameters that can be relied on, so you can use fetcher

import { useFetch, fetcher } from 'dahlia/rest'

const Todos = (a)= > {
  const { loading, data, error } = useFetch('/todos', { name: 'GetTodos' })

  if (loading) return <span>loading...</span>
  if (error) return <span>error!</span>

  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>)}const Refresh = (a)= > (
  <button onClick={()= > fetcher.GetTodos.refetch()}>refresh</button>
)

const TodoApp = (a)= > (
  <div>
    <Refresh />
    <Todos />
  </div>
)
Copy the code

See online Demo:

Fetcher [‘name’].refetch(), where refetch is the same function as internal refetch, so it also has options.

conclusion

In my opinion, asynchronous data should not be maintained using state management and should be kept within components. For most Web applications, data in state management should be a thin layer, and the side effects of dealing with asynchrony in state management should be avoided. Perhaps Redux’s default dissupport of processing asynchronous data was a rather far-sighted decision.

We found that using Hooks to manage asynchronous data, the code was very clean and had a minimalist, back-to-basics feel. A few lines of code can write the function, why do we need to make such a long link, make so round the logic.