# 在 react 中如何获取数据

翻译自:https://www.robinwieruch.de/react-fetching-data

React 的新手通常从不需要获取数据的项目开始。通常他们会面对像 Counter、Todo 或 TicTacToe 应用程序。这很好,因为在 React 迈出第一步时,获取数据将会为应用增加另一层复杂度。

但是,有时你希望从自己或第三方的 api 获取真实数据。这篇文章将带你演练在 React 中获取数据。在这里不会使用像 redux 或 mobx 这样的外部状态管理方案来存储你获取的数据。相反,你将使用 react 的本地状态管理。

# 在组件树中哪个位置获取数据

假设您已经有一个组件树,其层次结构中有多个级别的组件。 现在您将要从第三方 API 获取项目列表。 您的组件层次结构中的哪个层级,更准确地说,现在应该在哪个特定组件中获取数据? 基本上它取决于三个标准:

# 1. 谁对这些数据有兴趣

获取数据的组件应该是所有的这些组件的公共父组件

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

# 2. 当从异步请求中获取数据的等待过程中,你希望在哪里显示条件加载指示器(加载指示器、进度条)

加载指示器应该在第一个标准的公共父组件中显示,那么仍然应该在公共父组件中获取数据

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

但是当加载指示器应该显示在更顶级的组件中时,数据获取需要被提升到这个组件。

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

当加载指示器应该显示在公共父组件的子组件中时,不一定是需要数据的组件,公共父组件仍然是获取数据的组件。 然后可以将加载指示器状态传递给所有有兴趣显示加载指示器的子组件。

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

# 3. 当请求失败时,希望在哪里显示错误信息

这里和第二个标准的加载指示器规则相同

# 总结

这基本上就是在所有组件的层次中获取数据的所有内容了。但是,在公共父组件达成一致后,何时获取数据以及如何获取数据呢?

# 如何在 React 中获取数据

React 的 ES6 类组件有生命周期方法。render() 生命周期方法是必须输出 React 元素的,因为毕竟您可能希望在某些时候显示所获取的数据。

还有另一种生命周期方法非常适合获取数据:componentDidMount()。当这个方法运行时,组件已经用 render() 方法渲染了一次,但是当获取的数据通过 setState() 存储在组件的本地状态时,它会再次渲染。之后,可以在 render() 方法中使用本地状态来显示它或将其作为 props 传递。

componentDidMount() 生命周期方法是获取数据的最佳位置。 但究竟如何获取数据呢? React 的生态系统是一个灵活的框架,因此您可以选择自己的解决方案来获取数据。 为简单起见,本文将使用浏览器自带的原生 fetch API 来展示。 它使用 JavaScript 承诺来解决异步响应。 获取数据的最小示例如下:

import React, { Component } from 'react';

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

    this.state = {
      data: null,
    };
  }

  componentDidMount() {
    fetch('https://api.mydomain.com')
      .then(response => response.json())
      .then(data => this.setState({ data }));
  }

  ...
}

export default App;

这是最基本的 React.js fetch API 示例。 它向您展示了如何从 API 获取 React 中的 JSON。 但是,本文将使用真实世界的第三方 API 来演示它:

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() 方法存储在本地状态中。 然后 render() 方法将再次触发,您可以显示获取的数据。

...

class App extends Component {
 ...

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

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

export default App;

即使 render() 方法在 componentDidMount() 方法之前已经运行过一次,您也不会遇到任何空指针异常,因为您已经使用空数组初始化了本地状态中的 hits 属性。

注意:如果您想了解使用名为 React Hooks 的功能获取数据,请查看此综合教程:如何使用 React Hooks 获取数据? (opens new window)

# 加载状态和错误处理

当然,你需要在本地保存获取到的数据,但还有什么呢?你还可以在状态中保存另外两个属性:加载状态和错误状态;两者都会改善你的应用终端用户的用户体验

加载状态应该用于指示正在发生异步请求。 在两个 render() 方法之间,由于异步到达,获取的数据处于挂起状态。 因此,您可以在等待期间添加加载指示器。 在您的获取生命周期方法中,您必须将属性从 false 切换为 true,以及何时将数据从 true 切换为 false

...

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 的条件渲染 (opens new window) 来显示加载指示器或解析的数据。

...

class App extends Component {
  ...

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

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

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

加载指示器可以像 Loading... 一样简单,但您也可以使用第三方库来显示加载指示器或 挂起的内容组件 (opens new window)。 由你向终端用户发出数据加载中的信号。

您可以在本地状态中保存的第二个状态是错误状态。 当您的应用程序中发生错误时,没有什么比不向您的用户提供有关错误的指示更糟糕的了。

...

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

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

  ...

}

使用 promise 时,通常在 then() 块之后使用 catch() 块来处理错误。 这就是它可以用于原生 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 块。 例如,当 HTTP 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 (
      <ul>
        {hits.map(hit =>
          <li key={hit.objectID}>
            <a href={hit.url}>{hit.title}</a>
          </li>
        )}
      </ul>
    );
  }
}

以上就是使用简单的 React 进行数据获取的基础知识。你可以在 React 的本地状态或库(如Redux 之路 (opens new window))中阅读更多关于管理获取数据的内容。

# 如何在 React 中使用 AXIOS 获取数据

如前所述,您可以用另一个库替换 fetch API。 例如,另一个库可能会针对每个进入 catch 块的错误请求自行运行,而您不必首先抛出错误。 作为获取数据的库的一个很好的候选者是 axios。 您可以使用 npm install axios 在您的项目中安装 axios,然后在您的项目中使用它代替 fetch API。 让我们重构之前的项目,使用 axios 代替原生的 fetch API 在 React 中请求数据。

import React, { Component } from 'react';
import axios from 'axios';

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: [],
      isLoading: false,
      error: null,
    };
  }

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

    axios.get(API + DEFAULT_QUERY)
      .then(result => this.setState({
        hits: result.data.hits,
        isLoading: false
      }))
      .catch(error => this.setState({
        error,
        isLoading: false
      }));
  }

  ...
}

export default App;

如你所见, axios 也返回一个 promise。但这一次你不必两次解析 promise,因为 axios 已经为你返回了 JSON 响应。此外,在使用 axios 时,你可以确保所有的错误都在 catch() 块中捕获,另外,需要对返回的 axios 数据稍微调整数据结构

前面的示例仅向您展示了如何使用 React 的 componentDidMount 生命周期方法中的 HTTP GET 方法从 API 中获取数据。 但是,您也可以通过单击按钮主动请求数据。 那么你不会使用生命周期方法,而是你自己的类方法。

import React, { Component } from 'react';
import axios from 'axios';

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: [],
      isLoading: false,
      error: null,
    };
  }

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

    axios.get(API + DEFAULT_QUERY)
      .then(result => this.setState({
        hits: result.data.hits,
        isLoading: false
      }))
      .catch(error => this.setState({
        error,
        isLoading: false
      }));
  }

  ...
}

export default App;

但这只是 React 中的 GET 方法。 将数据写入 API 怎么样? 当有 axios 时,你也可以在 React 中做一个 post 请求。 您只需要将 axios.get()axios.post() 交换。

# 如何在 React 中测试数据获取?

那么如何测试来自 React 组件的数据请求呢? 有一个关于这个主题的广泛的 React 测试教程,但这里是简而言之。 当你使用 create-react-app 设置你的应用程序时,它已经带有 Jest 作为测试运行器和断言库。 否则,您也可以将 Mocha(测试运行程序)和 Chai(断言库)用于这些目的(请记住,测试运行程序和断言的功能那时会有所不同)。

在测试 React 组件时,我经常使用 Enzyme 来渲染我的测试用例中的组件。 此外,在测试异步数据获取时,Sinon 有助于监视和模拟数据。

npm install enzyme enzyme-adapter-react-16 sinon --save-dev

完成测试设置后,您可以为 React 场景中的数据请求编写第一个测试套件。

mport React from 'react';
import axios from 'axios';

import sinon from 'sinon';
import { mount, configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import App from './';

configure({ adapter: new Adapter() });

describe('App', () => {
  beforeAll(() => {

  });

  afterAll(() => {

  });

  it('renders data when it fetched data successfully', (done) => {

  });

  it('stores data in local state', (done) => {

  });
});

一个测试用例应该显示数据在获取数据后在 React 组件中成功渲染,而另一个测试验证数据是否存储在本地状态中。 也许测试这两种情况都是多余的,因为在渲染数据时,它也应该存储在本地状态中,但是为了演示它,您将看到两个用例。

在所有测试之前,您希望使用模拟数据保存您的 axios 请求。 您可以为它创建自己的 promise,并在以后使用它来对其解析功能进行细粒度控制。

...

describe('App', () => {
  const result = {
    data: {
      hits: [
        { objectID: '1', url: 'https://blog.com/hello', title: 'hello', },
        { objectID: '2', url: 'https://blog.com/there', title: 'there', },
      ],
    }
  };

  const promise = Promise.resolve(result);

  beforeAll(() => {
    sinon
      .stub(axios, 'get')
      .withArgs('https://hn.algolia.com/api/v1/search?query=redux')
      .returns(promise);
  });

  afterAll(() => {
    axios.get.restore();
  });

  ...
});

在所有测试之后,您应该确保再次从 axios 中删除存根。 这就是异步数据获取测试设置。 现在让我们实现第一个测试:

...

describe('App', () => {
  ...

  it('stores data in local state', (done) => {
    const wrapper = mount(<App />);

    expect(wrapper.state().hits).toEqual([]);

    promise.then(() => {
      wrapper.update();

      expect(wrapper.state().hits).toEqual(result.data.hits);

      done();
    });
  });

  ...
});

在测试中,您开始使用 Enzyme 的 mount() 函数渲染 React 组件,该函数确保执行所有生命周期方法并渲染所有子组件。 最初,您可以将您的命中断言作为组件本地状态中的空数组。 这应该是正确的,因为您使用一个空数组为 hits 属性初始化了本地状态。 一旦您实现了 promise 并手动触发了组件的渲染,状态应该在数据获取后发生了变化。

接下来,您可以测试是否所有内容都相应地渲染。 测试与之前的测试类似:

...

describe('App', () => {
  ...

  it('renders data when it fetched data successfully', (done) => {
    const wrapper = mount(<App />);

    expect(wrapper.find('p').text()).toEqual('Loading ...');

    promise.then(() => {
      wrapper.update();

      expect(wrapper.find('li')).toHaveLength(2);

      done();
    });
  });
});

在开始测试之前,加载指示器应该被渲染。同样的,一旦完成了 promise 并手动触发了组件的渲染,所请求的数据应该有两个列表数据。

这基本上是您在 React 中测试数据获取需要了解的内容。 它不需要很复杂。 通过您自己的 promise,您可以细粒度控制何时解决 promise 以及何时更新组件。 之后,您可以进行断言。 前面显示的测试场景只是一种方法。 例如,对于测试工具,您不一定需要使用 Sinon 和 Enzyme。

# 如何在 React 中使用 ASYNC/AWAIT 获取数据?

到目前为止,你只使用了 promise 的 then() 和 catch() 处理请求,javascript 的下一代异步请求怎么用?让我们将前面在 react 的获取数据实例重构为 async/await

import React, { Component } from 'react';
import axios from 'axios';

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

class App extends Component {
  ...

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

    try {
      const result = await axios.get(API + DEFAULT_QUERY);

      this.setState({
        hits: result.data.hits,
        isLoading: false
      });
    } catch (error) {
      this.setState({
        error,
        isLoading: false
      });
    }
  }

  ...
}

export default App;

在 React 中获取数据时,您可以使用 async/await 语句代替 then()。 async 语句用于表示函数是异步执行的。 它也可以用于 (React) 类组件的方法。 每当异步执行某些内容时,就会在 async 函数中使用 await 语句。 因此,在等待的请求解决之前,不会执行下一行。 此外,如果请求失败,可以使用 try 和 catch 块来捕获错误。

# 如何在高阶组件中获取数据?

之前展示的获取数据的方法在许多组件中使用时可能会重复。 组件挂载后,您希望获取数据并根据条件显示加载或错误指示器。 到目前为止,该组件可以分为两个职责:使用条件渲染显示获取的数据和获取远程数据,然后将其存储在本地状态。 前者仅用于渲染目的,后者可以由高阶组件 (opens new window)重用。

注意:当您要阅读链接的文章时,您还将看到如何抽象出高阶组件中的条件渲染。 之后,您的组件将只关心显示获取的数据,而无需任何条件渲染。

那么你将如何引入这种抽象的高阶组件来为你处理 React 中的数据获取。 首先,您必须将所有获取和状态逻辑分离到一个更高阶的组件中。

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

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

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

      axios
        .get(url)
        .then((result) =>
          this.setState({
            data: result.data,
            isLoading: false,
          })
        )
        .catch((error) =>
          this.setState({
            error,
            isLoading: false,
          })
        );
    }

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

除了渲染之外,高阶组件中的所有其他内容都取自前一个组件,在该组件中数据获取发生在该组件中。 此外,高阶组件接收将用于请求数据的 url。 如果稍后需要将更多查询参数传递给高阶组件,则始终可以扩展函数签名中的参数。

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

此外,高阶组件在本地状态中使用称为数据的通用数据容器。 它不再像以前一样知道特定的属性命名(例如 hits)。

在第二步中,您可以处理来自 App 组件的所有获取和状态逻辑。 因为它不再有本地状态或生命周期方法,您可以将其重构为功能性无状态组件。 传入属性从特定命中更改为通用数据属性。

const App = ({ data, isLoading, error }) => {
  if (!data) {
    return <p>No data yet ...</p>;
  }

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

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

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

最后但并非最不重要的是,您可以使用高阶组件来包装您的 App 组件。

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

...

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

基本上就是抽象出 React 中获取的数据。 通过使用高阶组件来获取数据,您可以轻松地为具有任何终端 API url 的任何组件选择加入此功能。 此外,您可以使用前面显示的查询参数对其进行扩展。

# 如何在 render props 中获取数据

高阶组件的另一种方式是 React 中的 render prop 组件。 也可以使用 render prop 组件在 React 中获取声明性数据。

class Fetcher extends React.Component {
  constructor(props) {
    super(props);

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

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

    axios
      .get(this.props.url)
      .then((result) =>
        this.setState({
          data: result.data,
          isLoading: false,
        })
      )
      .catch((error) =>
        this.setState({
          error,
          isLoading: false,
        })
      );
  }

  render() {
    return this.props.children(this.state);
  }
}

然后,您将能够在您的 App 组件中以以下方式使用 render props 组件:

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

...

const RenderPropApproach = () =>
  <Fetcher url={API + DEFAULT_QUERY}>
    {({ data, isLoading, error }) => {
      if (!data) {
        return <p>No data yet ...</p>;
      }

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

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

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

通过使用 React 的 children 属性作为渲染属性,您可以从 Fetcher 组件传递所有本地状态。 这就是您可以在 render props 组件中进行所有条件渲染和最终渲染的方式。

# 如何在 React 中从 GRAPHQL API 获取数据?

最后但并非最不重要的一点是,这篇文章应该简短地提到 React 的 GraphQL API。 您将如何从 GraphQL API 而非 React 组件的 REST API(您目前使用过)获取数据? 基本上可以用同样的方式实现,因为 GraphQL 对网络层没有自以为是。 大多数 GraphQL API 都通过 HTTP 公开,无论是否可以使用本机 fetch API 或 axios 查询它们。 如果您对如何在 React 中从 GraphQL API 获取数据感兴趣,请转到这篇文章:完整的 React 与 GraphQL 教程 (opens new window)

# 最后

您可以在此 GitHub 存储库 (opens new window) 中找到完成的项目。 对于 React 中的数据获取,您还有其他建议吗? 请联系我。 如果您将这篇文章分享给其他人以了解 React 中的数据获取,这对我来说意义重大。