如何在 React 中获取数据

React 的新手通常从根本不需要数据获取的应用程序开始。通常他们会遇到 Counter、Todo 或 TicTacToe 应用程序。这很好,因为在 React 中迈出第一步时,数据获取会为您的应用程序增加另一层复杂性。

但是,有时您想从自己的或第三方 API 请求真实世界的数据。本文将向您介绍如何在 React 中获取数据。没有外部状态管理解决方案,例如
Redux 或 MobX,用于存储您获取的数据。相反,您将使用 React 的本地状态管理。

在 React 的组件树中从哪里获取?

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

1. 谁对这些数据感兴趣?
获取组件应该是所有这些组件的公共父组件。

                      +---------------+
                      |               |
                      |               |
                      |               |
                      |               |
                      +------+--------+
                             |
                   +---------+------------+
                   |                      |
                   |                      |
           +-------+-------+     +--------+------+
           |               |     |               |
           |               |     |               |
           |  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 中获取数据?

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

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

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 获取 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 获取数据?

加载微调器和错误处理呢?

当然,您需要获取本地状态的数据。 但还有什么? 您可以在状态中存储另外两个属性:加载状态和错误状态。
两者都将改善应用程序最终用户的用户体验。

加载状态应该用于指示正在发生异步请求。 在两个 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 的条件渲染来显示加载指示器或解析的数据。

...

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… 消息一样简单,但您也可以使用第三方库来显示微调器或待处理的内容组件。
您可以向最终用户发出信号,表明数据提取处于待处理状态。

您可以保留在本地状态中的第二种状态是错误状态。 当您的应用程序中发生错误时,没有什么比不告诉您的最终用户错误更糟糕的了。

...

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 获取数据的基础知识。 您可以在 The Road to Redux 中阅读更多关于在 React 的本地状态或 Redux 等库中管理获取的数据的信息。

如何在 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 也返回一个 JavaScript 承诺。 但是这次你不必两次解析promise,因为axios已经为你返回了一个JSON响应。 此外,在使用 axios 时,您可以确保在 catch() 块中捕获所有错误。 另外,对于返回的axios数据,还需要稍微调整一下数据结构。
前面的示例仅向您展示了如何使用 React 的 componentDidMount 生命周期方法中的 HTTP GET 方法从 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,
    };
  }

  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 中执行发布请求。 您只需要将 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 场景中的数据请求编写你的第一个测试套件。

import 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 请求。 您可以为它创建自己的 JavaScript
承诺,并在以后使用它来对其解析功能进行细粒度控制。

...

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 属性的空数组初始化本地状态。 一旦您解决了承诺并手动触发组件的渲染,状态应该在数据获取后发生了变化。

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

...

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();
    });
  });
});

在测试开始时,应呈现加载指示器。 同样,一旦您解决了承诺并手动触发组件的渲染,所请求的数据应该有两个列表元素。

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

如何在 React 中使用 Async/Await 获取数据?

到目前为止,您只使用了处理 JavaScript 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 块来捕获错误。

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

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

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

那么你将如何引入这样抽象的高阶组件来为你处理 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) =>
  ...

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

在第二步中,您可以从您的 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中获取data

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

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 prop 组件:

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 属性作为 render prop,您可以从 Fetcher 组件传递所有本地状态。 这就是您可以在您的渲染道具组件中进行所有条件渲染和最终渲染的方式。

如何从 React 中的 GraphQL API 获取数据?

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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值