React 異步數據管理思考

異步數據管理一直是前端的一個重點和難點,能夠這麼說,80%的 web 應用會有異步數請求據並在 UI 中消費,而且在至關多的 web 應用中,處理異步數據是它的核心業務邏輯。前端

在 React 的生態圈中,大部分人把異步數據使用狀態管理維護,好比使用 Redux,用異步 Action 獲取遠程數據,而後存在 store 中。react

但在這個時間節點,9012 年了,我認爲使用狀態管理去維護異步數據不是一種優雅的方式,React Hooks 出現後,我認爲直接在組件內維護異步數據更加合理。無論從開發效率仍是可維護性看,都比使用狀態管理好。git

爲何這說呢?下面咱們經過代碼來看看。github

如今,假設咱們要實現一個功能,獲取一個 TodoList 數據,而且用組件渲染。web

最簡單是直接在組件內使用生命週期獲取數據,而後存在組件內部的 state 中。json

使用 React 生命週期

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>
    )
  }
}
複製代碼

在線 Demo 請看: redux

Edit fetch-data-with-cdm

這種方式很是很是符合人的直覺,但最大的問題是:外部沒法改變異步數據,組件渲染後數據就沒法再改變。這也是大部分人使用狀態管理維護異步數據的原因。異步

下面咱們看看如何使用 Redux 維護異步數據。async

使用 Redux

假設咱們已經使用了 Redux 中間件 redux-thunk,咱們會有下面相似的代碼:函數

首先,咱們會把字符串定義定義爲常量到一個 constant.js

export const LOADING_TODOS = 'LOADING_TODOS'
export const LOAD_TODOS_SUCCESS = 'LOAD_TODOS_SUCCESS'
export const LOAD_TODOS_ERROR = 'LOAD_TODOS_ERROR'
複製代碼

而後,編寫異步的 action, 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,
        })
      })
  }
}
複製代碼

接着,在 reducer 中處理數據,todos.js

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
  }
}
複製代碼

還沒完,最後,在組件中使用:

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)
複製代碼

在線 Demo 請看:

Edit fetch-data-with-redux

咱們能夠發現,使用 Redux 管理異步數據,代碼量激增,囉嗦冗餘,模板代碼一堆,,無論開發效率仍是開發體驗,亦或是能夠維護性和可讀性,我的認爲,相似的 redux 這樣的解決方案並不優雅。

下面咱們看看如何使用 React Hooks 獲取異步數據。

使用 React Hooks

咱們使用 一個庫叫dahlia-restuseFetch 獲取數據,能夠輕鬆的拿到數據的狀態 { loading, data, error },而後渲染處理:

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

const Todos = () => {
  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
複製代碼

dahlia-rest的完整用法能夠看 dahlia-rest

在線 Demo 請看:

Edit fetch-data-with-dahlia

代碼很是簡潔,loading 狀態和錯誤處理很是優雅,也許你發現了,貌似這也和使用生命週期同樣,外部沒法改變數據狀態,其實不是的,下面重會點講講如何更新數據。

Hooks 更新異步數據

使用 hooks 維護異步數據,有三種方式更新異步數據,這裏用 dahlia-rest 舉例。

內部 refetch

這是最簡單的從新獲取數據的方式,一般,若是觸發更新的動做和useFetch在統一組件內,可使用這種方式。

const Todos = () => {
  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 = () => {
    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>
  )
}
複製代碼

在線 Demo 請看:

Edit refetch

更新依賴 (deps)

經過更新依賴來從新獲取數據,這也是經常使用的方式之一,由於在不少業務場景中,觸發動做會在其餘組件中,下面演示如何經過更新依賴觸發數據更新:

這裏使用一個簡單的狀態管理庫維護依賴對象,狀態管理的完整文檔請看dahlia-store

定義一個 store 用來存放依賴:

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

const todoStore = createStore({
  params: {
    _start: 0,
    _limit: 5,
  },
  updateParams(params) {
    todoStore.params = params
  },
})
複製代碼

在組件中,使用依賴:

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

const Todos = observe(() => {
  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 = () => {
    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>
  )
})
複製代碼

在線 Demo 請看:

Edit deps

你能夠在任意地方,無論組件內仍是組件外,你均可以能夠調用todoStore.updateParams更新依賴,從而實現數據更新。

注意:這裏的依賴是個對象,你必須更新整個對象的引用,若是你只更新對象的屬性是無效的。

使用 fetcher

有時候,你須要在組件外部從新獲取數據,但useFetch 卻沒有任何能夠被依賴的參數,這時你可使用 fetcher

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

const Todos = () => {
  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 = () => (
  <button onClick={() => fetcher.GetTodos.refetch()}>refresh</button>
)

const TodoApp = () => (
  <div> <Refresh /> <Todos /> </div>
)
複製代碼

在線 Demo 請看:

Edit fetcher

使用 fetcher 是,你須要爲useFetch 提供 name 參數,用法是:fetcher['name'].refetch(),這裏的 refetch 和內部 refetch 是同一個函數,因此它也有 options 參數。

總結

我的認爲,異步數據不該該使用狀態管理來維護,應該放在組件內。對於大多數 web 應用,狀態管理中的數據應該是比較薄的一層,而且應該避免在狀態管理中處理異步帶來的反作用。也許,Redux 默認不支持處理異步數據,是一個至關有遠見的決定。

咱們發現,使用 Hooks 管理異步數據,代碼很是簡潔,有一種大道至簡感受和返璞歸真感受。幾行代碼就能寫完功能,爲何要搞出那麼長的鏈路,搞那麼繞的邏輯。

相關文章
相關標籤/搜索