【译】React如何获取数据

5,109

原文链接:How to fetch data in React
作者:rwieruch

刚开始使用React做项目的新手并不需要获取数据,通常他们制作一些类似计数器、Todo或井字棋应用。因为在刚开始学习React时候,获取数据通常会增加复杂性。

然而,在某一时刻你想从第三方API获取真实数据,本文会讲解如何在原生React中获取数据。没有额外的状态管理方法参与储存获取来的数据,只好使用React本地状态管理。

在React组件树中哪里能获取数据

设想你已经有一个几层层次结构的组件树。现在你将要从第三方API中获取一系列的元素。在组件层的哪一层,准确的说是哪个指定的组件中能获取数据?基本上取决于三个条件:

  1. 谁需要这数据?fetch组件应该是所有这些需要数据的组件的父组件。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+
  2. 当你正从从异步请求中获取数据时,你想在哪里显示加载指示器(如加载转轮,进度条)?根据第一条准则,加载指示器应该显示在共同父组件中,接着共同的父组件仍然是用来抓取数据的组件。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |  Loading ...  |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+

    2.1 但是当加载指示器显示在更高层级组件中时,抓取数据需要提升至这个组件。

                       +---------------+
                       |               |
                       |               |
                       |  Fetch here!  |
                       |  Loading ...  |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |               |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |               |     |               |     |               |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+

    2.2 当加载指示器需要显示在共同父组件的子组件时,共同父组件仍是获取数据的组件。加载指示器状态传递到所有加载指示器的子组件中。

                       +---------------+
                       |               |
                       |               |
                       |               |
                       |               |
                       +------+--------+
                              |
                    +---------+------------+
                    |                      |
                    |                      |
            +-------+-------+     +--------+------+
            |               |     |               |
            |               |     |               |
            |  Fetch here!  |     |               |
            |               |     |               |
            +-------+-------+     +---------------+
                    |
        +-----------+----------+---------------------+
        |                      |                     |
        |                      |                     |
    +------+--------+     +-------+-------+     +-------+-------+
    |               |     |               |     |               |
    |               |     |               |     |               |
    |    I am!      |     |               |     |     I am!     |
    |  Loading ...  |     |  Loading ...  |     |  Loading ...  |
    +---------------+     +-------+-------+     +---------------+
                               |
                               |
                               |
                               |
                       +-------+-------+
                       |               |
                       |               |
                       |     I am!     |
                       |               |
                       +---------------+
  3. 当请求失败时候,你想在哪里显示错误信息?在这里,第二个标准同样适用于这种情况。

这就是基本的在哪里获取数据的准则。但是一旦父组件同意后如何获取呢?

如何获取React的数据

React的ES6类组件有生命周期函数。render()生命周期函数用于输出React组件的,因为毕竟你想在某一时刻显示抓取的数据。

还有另一个生命周期函数完美的适合获取数据:componentDidMount()。当这个方法运行时,组件已经用render()方法渲染完毕了,但是当获取来的数据通过setState()方法存储到本地state时会再次渲染组件一次。后来,本地状态会在render()方法中被用于渲染或者作为props传递。

componentDidMount()生命函数方法是最好获取数据的地方。但是如何获取数据呢?React的生态系统是灵活的框架,因此你可以选择你自己的方法获取数据。为了简单起见,本文会使用原生的fetch API,它是使用JavaScript promises来解决异步请求。

import React, { Component } from 'react';

const API = 'https://hn.algolia.com/api/v1/search?query=';
const DEFAULT_QUERY = 'redux';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
    };
  }

  componentDidMount() {
    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits }));
  }

  ...
}

export default App;

本例采用了Hacker News API,但是可以随意使用自己的API端点。当数据获取成功后,会通过React的this.setState()存储在state中。接着render()方法会再次调用,然后显示被获取的数据。


class App extends Component {


  render() {
    const { hits } = this.state;

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}
export default App;

即使render()方法已经在componentDidMount()前运行一次了,你也不会遇到空指针异常,因为你已经用空数组中初始化了hits属性。

什么是加载转轮和错误处理?

当然你需要获取的数据。但是别的呢?在state中你需要存储两个更重要的属性:加载state和错误state。两者都会提高应用的用户体验。

加载state会被用于表明异步请求正在发生。在两个render之间获取的数据由于异步正在等待中,所以你可以在等待时间中增加一个加载指示器。在你获取的生命周期方法中,当你的数据处理完后,你不得不切换为true属性。

...

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
      isLoading: false,
    };
  }

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits, isLoading: false }));
  }

  ...
}

export default App;

render()方法中你可以使用React条件渲染方法去渲染加载指示器或已处理完的数据。

...

class App extends Component {
  ...

  render() {
    const { hits, isLoading } = this.state;

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}

加载指示器与加载信息一样简单,但是你可以使用第三方库来显示转轮或待完成内容组件。这取决于你是否要让你的终端用户知道数据在处理中。

你需要保存的第二个状态会是错误状态。当错误发生时,没有什么比不给你终端用户错误指示更糟糕的事情。

...

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hits: [],
      isLoading: false,
      error: null,
    };
  }

  ...

}

当使用promise,catch()块会通常在then()后使用来处理错误。这同样适用于原生的fetch API。

...

class App extends Component {

  ...

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => response.json())
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  ...

}

不幸的是,原生的fetch API不会对每个错误状态代码使用catch块。例如,当发生404错误时,不会进入catch块中,但是你可以通过抛出异常迫使其进入catch。

...

class App extends Component {

  ...

  componentDidMount() {
    this.setState({ isLoading: true });

    fetch(API + DEFAULT_QUERY)
      .then(response => {
      //如果正常,则进行处理,否则抛出异常
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Something went wrong ...');
        }
      })
      .then(data => this.setState({ hits: data.hits, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  ...

}

最后,你可以展示错误信息在你的render()方法作为条件渲染方法。

...

class App extends Component {

  ...

  render() {
    const { hits, isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }

    if (isLoading) {
      return <p>Loading ...</p>;
    }

    return (
      <div>
        {hits.map(hit =>
          <div key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </div>
        )}
      </div>
    );
  }
}

这些就是原生React中获取数据的基本方法。正如之前提及的,你可以使用第三方库代替原生fetch API。例如,其他库也许会针对每个错误请求,都会进入catch块,而不需要你自己抛出异常。

如何抽像数据获取部分

获取数据的显示方法在几个组件中一般是重复的。一旦组件安装上后,你想要获取数据和展示条件性的加载或错误的指示器。组件至今会被分为两个职责:展示抓取的数据和抓取state。后者一般可以通过高阶组件重复使用。(如果你有兴趣读这篇文章,会发现从高阶组件中抽取条件性渲染。毕竟,你的组件会只关注与显示获取的数据)

首先,你不得不分裂所有获取部分和状态逻辑成高阶组件

const withFetching = (url) => (Comp) =>
  class WithFetching extends Component {
    constructor(props) {
      super(props);

      this.state = {
        data: {},
        isLoading: false,
        error: null,
      };
    }

    componentDidMount() {
      this.setState({ isLoading: true });

      fetch(url)
        .then(response => {
          if (response.ok) {
            return response.json();
          } else {
            throw new Error('Something went wrong ...');
          }
        })
        .then(data => this.setState({ data, isLoading: false }))
        .catch(error => this.setState({ error, isLoading: false }));
    }

    render() {
      return <Comp { ...this.props } { ...this.state } />
    }
  }

上面高阶组件收到一个url用于获取数据,这个url会成为特定的之前使用的API + DEFAULT_QUERY参数。如果你需要传递更多查询参数到你的高阶组件,你需要扩展函数参数。

const withFetching = (url, query) => (Comp) =>
  ...

另外,高阶组件使用数据存储器成为data。不用像以前那样担心特定的属性名了。

在第二步中,你可以在你App组件中暴露任何获取方法和状态逻辑。因为这个组件不再有本地state和生命周期函数,你可以重构为无状态函数组件。即将到来属性会将特定的hits改变为普遍的data属性。

const App = ({ data, isLoading, error }) => {
  const hits = data.hits || [];

  if (error) {
    return <p>{error.message}</p>;
  }

  if (isLoading) {
    return <p>Loading ...</p>;
  }

  return (
    <div>
      {hits.map(hit =>
        <div key={hit.objectID}>
          <a href={hit.url}>{hit.title}</a>
        </div>
      )}
    </div>
  );
}

最后,你可以使用高阶组件去包裹App组件:

const AppWithFetch = withFetching(API + DEFAULT_QUERY)(App);

这基本上就是抽象数据获取。通过使用高阶组件去获取数据,你可以很容易的加入特性到任何终端API url的组件。除此之外,你可以加入查询参数扩展组件。

虽然你不需要知道通过高阶组件抽象数据获取部分,但是我希望您能学会React中数据获取的基本部分,你可以通过GitHub repository获得全部代码。


欢迎订阅掘金专栏知乎专栏,关注个人博客