异步数据管理一直是前端的一个重点和难点,能够这么说,80%的 web 应用会有异步数请求据并在 UI 中消费,而且在至关多的 web 应用中,处理异步数据是它的核心业务逻辑。前端
在 React 的生态圈中,大部分人把异步数据使用状态管理维护,好比使用 Redux,用异步 Action 获取远程数据,而后存在 store 中。react
但在这个时间节点,9012 年了,我认为使用状态管理去维护异步数据不是一种优雅的方式,React Hooks 出现后,我认为直接在组件内维护异步数据更加合理。无论从开发效率仍是可维护性看,都比使用状态管理好。git
为何这说呢?下面咱们经过代码来看看。github
如今,假设咱们要实现一个功能,获取一个 TodoList 数据,而且用组件渲染。web
最简单是直接在组件内使用生命周期获取数据,而后存在组件内部的 state 中。json
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> ) } }
这种方式很是很是符合人的直觉,但最大的问题是:外部没法改变异步数据,组件渲染后数据就没法再改变。这也是大部分人使用状态管理维护异步数据的原因。异步
下面咱们看看如何使用 Redux 维护异步数据。async
假设咱们已经使用了 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)
咱们能够发现,使用 Redux 管理异步数据,代码量激增,啰嗦冗余,模板代码一堆,,无论开发效率仍是开发体验,亦或是能够维护性和可读性,我的认为,相似的 redux 这样的解决方案并不优雅。
下面咱们看看如何使用 React Hooks 获取异步数据。
咱们使用 一个库叫dahlia-rest
的 useFetch
获取数据,能够轻松的拿到数据的状态 { 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
代码很是简洁,loading 状态和错误处理很是优雅,也许你发现了,貌似这也和使用生命周期同样,外部没法改变数据状态,其实不是的,下面重会点讲讲如何更新数据。
使用 hooks 维护异步数据,有三种方式更新异步数据,这里用 dahlia-rest
举例。
这是最简单的从新获取数据的方式,一般,若是触发更新的动做和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> ) }
经过更新依赖来从新获取数据,这也是经常使用的方式之一,由于在不少业务场景中,触发动做会在其余组件中,下面演示如何经过更新依赖触发数据更新:
这里使用一个简单的状态管理库维护依赖对象,状态管理的完整文档请看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> ) })
你能够在任意地方,无论组件内仍是组件外,你均可以能够调用todoStore.updateParams
更新依赖,从而实现数据更新。
注意:这里的依赖是个对象,你必须更新整个对象的引用,若是你只更新对象的属性是无效的。
有时候,你须要在组件外部从新获取数据,但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> )
使用 fetcher 是,你须要为useFetch
提供 name 参数,用法是:fetcher['name'].refetch()
,这里的 refetch
和内部 refetch
是同一个函数,因此它也有 options 参数。
我的认为,异步数据不该该使用状态管理来维护,应该放在组件内。对于大多数 web 应用,状态管理中的数据应该是比较薄的一层,而且应该避免在状态管理中处理异步带来的反作用。也许,Redux 默认不支持处理异步数据,是一个至关有远见的决定。
咱们发现,使用 Hooks 管理异步数据,代码很是简洁,有一种大道至简感受和返璞归真感受。几行代码就能写完功能,为何要搞出那么长的链路,搞那么绕的逻辑。