您当前的位置:首页 > 网站建设 > React
| php | asp | css | H5 | javascript | Mysql | Dreamweaver | Delphi | 网站维护 | 帝国cms | React | 考试系统 | ajax | jQuery |

React hook 中的数据获取

51自学网 2020-03-09 17:20:03
  React

React hook 中的数据获取


翻译葡萄糖o_o 最后发布于2019-08-03 14:04:38 阅读数 705  收藏
展开

相关说明:

  1. 对于hook相关词不翻译,感觉翻译后怪怪的。

  2. effect hook 效果钩子,用于执行一些副作用例如获取数据 。

  3. state hook 状态钩子。

  4. 使用----------- 和 ----------- 标出代码需要关注的地方。

渣翻译如下:

在这个指南中,我想给你展示使用state和effect hook在React hooks中如何获取数据。我们将使用著名的 Hacker News API从高科技世界中获取受欢迎的文章。你也可以为获取数据实现自定义获取数据的hook,这个hook可以在你的应用中任何地方重用,也可以作为一个独立的node包发布到npm上。

如果关于react的新特性你什么都不知道,可以查看这篇文章introduction to React Hooks。如果你想查看怎么通过React Hooks获取数据例子的完整项目,查看这个GitHub 仓库

如果你只是想在使用React Hook获取数据前有一个准备:npm install use-data-api 并且参照这个文档。如果你采用了不要忘了小星星哦:-)。

**注:**在未来,React没有计划为获取数据添加专门的Hooks。反而,Suspense将会负责这个功能。下面的预演是学习react中关于state和effect hooks一个比较好的方法。

使用React Hooks获取数据

如果你不熟悉在React中获取数据,查看我的在react中获取大量的数据这篇文章。这篇文章会引导你使用React Comopnent 类获取数据,怎么样可以让获取数据的逻辑通过 Render Prop Components 和 Higher-Order Components重用,并且怎么处理重用加载出错和加载中的状态。在这篇文章中,我想给你展示以上这些通过React Hooks在函数式组件中的做法。

import React, { useState } from 'react';

function App() {
  const [data, setData] = useState({ hits: [] });

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

export default App
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这个App组件展示了项目列表(hits 是 Hacker News 的文章)。这个state和跟新state的函数来自于状态钩子useState的调用,它的责任是管理本地我们将要为App组件获取的数据数据的状态,初始状态的数据是一个对象中的空列表。还没有人为这个数据设置任何状态。

我们将使用axios去获取数据,但是是使用其他获取数据的库还是使用浏览器原生的fetch API由你决定。如果你还没有安装axios,你可以在命令行输入npm install axios。然后实现你自己的获取数据的effect hook。

// -------------------------------------------------
import React, { useState, useEffect } from 'react';
import axios from 'axios';
// -------------------------------------------------

function App() {
  const [data, setData] = useState({ hits: [] });
  
	// -------------------------------------------------
  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

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

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

名为useEffect的effect hook被用于使用axios从接口获取数据,并且通过状态钩子的更新函数设置数据到组件的本地状态中。promise 通过 async/await中 被 resolve。

然而,当你运行你的应用的的时候,你应该会陷入一个令人讨厌的循环。effect hook会在组件挂载的时候运行但是也会在组件跟新的时候运行。因为我们在每次获取数据之后设置状态,然后组件跟新然后effect hook再次运行。组件将会一次又一次的获取数据。这是一个需要避免的问题。我们只希望在组件挂载的时候获取数据。

这就是为什么你需要提供一个空数组作为effect hook的第二个参数的原因,是为了阻止在组件更新的时候激活它,只在组件挂载的时候激活它。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
  // -------------------------------------------------
  }, []);
  // -------------------------------------------------
  
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

第二个参数被用于定义钩子依赖的所有变量(分配到这个数组中)。如果有一个变量改变,钩子会再次运行。如果数组中没有变量,这个钩子在组件更新的时候就不会运行,因为它没有监听任何变量。

还有最后一个问题。在代码中,我们使用async/await从第三方接口获取数据。根据文档表述每个使用async注释的函数都会返回一个隐含的promise对象:async函数声明定义一个异步函数,返回一个异步函数对象。*An asynchronous function is a function which operates asynchronously via the event loop, *异步函数是一个操作通过事件循环操作异步的函数,使用隐式的Promise作为结果返回”。However, an effect hook should return nothing or a clean up function.然而,一个effect hook不应该返回值或者返回一个清除函数。(这是个啥,return nothing)这就是为什么在你的开发者日志里面能看见下面的警告: 07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.。这就是为什么不允许在useEffect直接使用异步函数的原因。让我们来修复它,通过异步函数取代effect hook。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    // -------------------------------------------------
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
    // -------------------------------------------------
  }, []);

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

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

简而言之这就是在React hooks中获取数据。但是如果你对错误处理,加载状态,怎么从表单触发数据获取,怎么实现一个重用的数据获取钩子感兴趣, 请继续阅读。

如何以编程的方式/手动触发钩子

很好,我们将会在组件挂载的时候获取一次数据。但是怎么使用输入的字段去告诉接口我们感兴趣的话题呢?“Redux“作为默认的查询。但是哪些话题是关于"React"的呢?让我们实现一个输入框去让人能够获取Redux以外的其他信息。因此为输入框引入一个新的状态。

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  // -------------------------------------------------
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <Fragment>
      {/* ------------------------------------------------- */}
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      {/* ------------------------------------------------- */}
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

目前,每个状态都是独立的,但是现在你想结合他们只获取通过输入框输入的查询字段指定文章。通过下面的改变,组件应该在挂载的时候通过查询字段获取一次所有文章。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        // -------------------------------------------------
        `http://hn.algolia.com/api/v1/search?query=${query}`,
        // -------------------------------------------------
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    ...
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

有一块被遗漏了:当你在输入框中输入内容的时候,在组件挂在之后effect hook不会获取其他数据。这是因为你用一个空数组作为effect hook函数的第二个参数。这个副作用就没有依赖的变量,所以它只在组挂载的时候触发。然而,现在effect hook应该依赖query。一旦query改变,就应该再次请求数据。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  // -------------------------------------------------
  }, [query]);
  // -------------------------------------------------

  return (
    ...
  );
}

export default App;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在你改变输入框中的值的时候应该获取一次数据。但是它带来了另一个问题:你在输入框中输入每一个字符都会触发并执行effect hook,然后执行获取其他数据。提供一个按钮去触发请求,手动触发钩子怎么样?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------
  const [search, setSearch] = useState('');
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      {/* ------------------------------------------------- */}
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>
      {/* ------------------------------------------------- */}

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

现在,让effect hook依赖search状态而不是根据输入的每个内容波动的query状态,用户点击一次按钮,新的search状态就会被设置并且应该手动触发一次effect hook。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------
  const [search, setSearch] = useState('redux');
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        // -------------------------------------------------
        `http://hn.algolia.com/api/v1/search?query=${search}`,
        // -------------------------------------------------
      );

      setData(result.data);
    };

    fetchData();
  // -------------------------------------------------
  }, [search]);
  // -------------------------------------------------

  return (
    ...
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

search的初始状态也应该和query的初始状态一样,因为组件也会在挂载的时候获取数据,因此结果应该和输入的一致。然而,query的search状态一样让人有点疑惑。为啥不把search的状态换成真实的URL呢?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      // -------------------------------------------------
      const result = await axios(url);
      // -------------------------------------------------

      setData(result.data);
    };

    fetchData();
  // -------------------------------------------------
  }, [url]);
  // -------------------------------------------------

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        {/* ------------------------------------------------- */}
        onChange={event => setQuery(event.target.value)}
        {/* ------------------------------------------------- */}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

这就是使用effect hook隐式获取数据的情况。你可以决定这个effect hook依赖哪个状态,一旦你在点击的时候或者其他副作用设置这个状态,这个effect hook将会再次执行。在这个案例中,如果URL状态改变了,effect hook会再次执行从接口中获取数据。

React Hooks中的加载指示

让我来介绍一个获取数据的加载指示器。它就是另一个状态钩子(state hook)管理的状态(state)。这个加载的标志被用于在App组件中渲染一个加载中的指示器。

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  // -------------------------------------------------
  const [isLoading, setIsLoading] = useState(false);
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      // -------------------------------------------------
      setIsLoading(true);
      // -------------------------------------------------

      const result = await axios(url);

      setData(result.data);
      // -------------------------------------------------
      setIsLoading(false);
      // -------------------------------------------------
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>
			{/* ------------------------------------------------- */}
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
      {/* ------------------------------------------------- */}
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      {/* ------------------------------------------------- */}
      )}
      {/* ------------------------------------------------- */}
    </Fragment>
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

当effect hook在组件挂在或者URL状态改变的时候被调用去获取数据,这个加载状态就会被设置为true。当请求完成了,这个加载状态就会再次被设置为false。

React Hooks的错误处理

在React hook怎么处理获取数据出错呢?这个错误只是通过另一个状态钩子初始化的。当这个状态表示出错了,这个 App组件可以给用户一个反馈。当使用 async/await,常用try/catch块去处理错误。你可以在effect hook里面这样做:

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  // -------------------------------------------------
  const [isError, setIsError] = useState(false);
  // -------------------------------------------------

  useEffect(() => {
    const fetchData = async () => {
      // -------------------------------------------------
      setIsError(false);
      // -------------------------------------------------
      setIsLoading(true);
			
      // -------------------------------------------------
      try {
      // -------------------------------------------------
        const result = await axios(url);

        setData(result.data);
      // -------------------------------------------------
      } catch (error) {
        setIsError(true);
      }
      // -------------------------------------------------

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>
      
	  {/* ------------------------------------------------- */}
      {isError && <div>Something went wrong ...</div>}
	  {/* ------------------------------------------------- */}
      
      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

这个错误的状态在钩子每次执行的时候都会被重置。这是很有用的,因为在失败的请求之后,用户回想再次尝试,应该重置错误状态。为了检查出错的情况,你可以将URL更改为无效的内容。然后查看错误消息是否显示。

通过React和表单的获取数据

在表单中如何获取数据?至今,我们只组合了input和按钮。当你引入了更多的输入元素,你就会想要使用表单元素包裹他们。另外,一个表单可能通过键盘的回车键触发按钮触发提交。

function App() {
  ...

  return (
    <Fragment>
      {/* ------------------------------------------------- */}
      <form
        onSubmit={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        {/* ------------------------------------------------- */}
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        {/* ------------------------------------------------- */}
        <button type="submit">Search</button>
      </form>
     	{/* ------------------------------------------------- */}

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

但是现在浏览器在你点击提交按钮的时候会刷新,因为这是一个浏览器提交表单的原生行为。为了阻止默认行为,我们可以调用React事件对象的函数。就像你在React类组件中做的那样。

function App() {
  ...

  return (
    <Fragment>
     	{/* ------------------------------------------------- */}
      <form onSubmit={event => {
      {/* ------------------------------------------------- */}
        setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
				{/* ------------------------------------------------- */}
        event.preventDefault();
      }}>
        {/* ------------------------------------------------- */}
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

现在点击提交按钮的时候浏览器就不会再刷新了。它就像之前那样工作,但是这次使用form替换了原生的输入字段和按钮的结合。你也可以在键盘上按回车键提交表单。

自定义获取数据钩子

为了提取一个自定义获取数据的钩子,移动每个属于数据获取数据的代码到自己的函数,除了属于输入字段的query状态,但是包含加载指示器和错误处理。也要确定你在函数中返回了所有App组件里必要的变量。

// -------------------------------------------------
const useHackerNewsApi = () => {
// -------------------------------------------------
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);
// -------------------------------------------------
  return [{ data, isLoading, isError }, setUrl];
}
// -------------------------------------------------
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

现在,你的新钩子在App组件中又可以使用了。

function App() {
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
	// -------------------------------------------------
  
  return (
    <Fragment>
      <form onSubmit={event => {
        {/* ------------------------------------------------- */}
        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
				{/* ------------------------------------------------- */}
          
        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      ...
    </Fragment>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

初始状态也可以通用,通过它简化新的自定义钩子。

import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';

// -------------------------------------------------
const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
// -------------------------------------------------
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

function App() {
  const [query, setQuery] = useState('redux');
  // -------------------------------------------------
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    'http://hn.algolia.com/api/v1/search?query=redux',
    { hits: [] },
  );
  // -------------------------------------------------

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79

通过自定义钩子获取数据。这个钩子自己不知道关于接口的任何信息。他接受所有从外面传入的参数并且只管理必要的状态,例如data,加载状态和错误状态。他执行请求和返回数据给把它当做自定义获取数据钩子使用的组件。

使用Reducer Hook获取数据

至今,我们使用各个state hooks 去管理我们数据的数据获取状态,加载和错误状态。然而,不知为啥,这些状态被自己的state hook管理,它们应该属于一起的因为它们关心相同的原因。就像你看到的一样它们都在数据获取函数中使用。它们是一起的一个很好的标志是它们一个接着一个的使用(e.g setIsErrorsetIsLoading)。让我们使用Reducer Hook结合并替换它们。

一个Reducer Hook 使用一个state对象和一个函数生成一个state对象。这个函数被称为 —— dispatch函数 —— 分发一个action,这个action里面有一个type属性和一个可选的payload对象。所有这些信息在真实的reducer函数中被使用去从之前的状态生成一个新的状态,所有信息表示为这个action的payload和type。让我们看看在代码中是怎么工作的。

import React, {
  Fragment,
  useState,
  useEffect,
  // -------------------------------------------------
  useReducer,
  // -------------------------------------------------
} from 'react';
import axios from 'axios';

// -------------------------------------------------
const dataFetchReducer = (state, action) => {
  ...
};
// -------------------------------------------------

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  
	// -------------------------------------------------
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  // -------------------------------------------------

  ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

这个Reducer Hook使用reducer函数和初始的状态对象作为参数。在我们的例子里,data的初始化状态,loading和error的初始状态没有改变,但是它们替换了单个state hooks,通过reducer hook汇总到一个state对象里面管理。

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
      // -------------------------------------------------
      dispatch({ type: 'FETCH_INIT' });
      // -------------------------------------------------

      try {
        const result = await axios(url);
				
        // -------------------------------------------------
        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        // -------------------------------------------------
      } catch (error) {
        // -------------------------------------------------
        dispatch({ type: 'FETCH_FAILURE' });
        // -------------------------------------------------
      }
    };

    fetchData();
  }, [url]);

  ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

现在,获取数据的时候可以使用dispatch函数发送一个信息给reducer函数。dispatch分发的对象有一个约定的type属性和一个可选的payload属性。这个type告诉reducer函数哪个状态需要改变和reducer可以使用payload去提取一个新的state。毕竟我们只有三个状态改变:初始的获取进程。通知成功的数据获取结果。通知失败的数据获取结果。

在自定义钩子的最后,这个state就像之前一样被返回出去,但是因为我们一整个state对象,所以再也没有独立的state了。这样,使用useDataApi自定义钩子的人还可以访问到 dataisLoading和 isError

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...
	
  // -------------------------------------------------
  return [state, setUrl];
  // -------------------------------------------------
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

最后但是很重要的是,缺少reducer函数的实现。它需要发出三个不同的状态转换,叫作 FETCH_INIT, FETCH_SUCCESS 和 FETCH_FAILURE。每个状态转换需要返回一个新的状态对象。让我们看看这个怎么通过switch case实现:

const dataFetchReducer = (state, action) => {
  // -------------------------------------------------
  switch (action.type) {
    case 'FETCH_INIT':
      return { ...state };
    case 'FETCH_SUCCESS':
      return { ...state };
    case 'FETCH_FAILURE':
      return { ...state };
    default:
      throw new Error();
  }
  // -------------------------------------------------
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

一个reducer函数可以通过它的arguments访问当前的state和action。现在switch case语句每个状态被转换只返回之前的state。使用解构语句去保证state对象不可变 - 意味着state是不能直接改变的 - 这是最佳实践。现在让我们覆盖一些当前的state需要被返回的属性来改变状态转换的状态:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
        // -------------------------------------------------
        isLoading: true,
        isError: false
        // -------------------------------------------------
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
        // -------------------------------------------------
        isLoading: false,
        isError: false,
        data: action.payload,
        // -------------------------------------------------
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
        // -------------------------------------------------
        isLoading: false,
        isError: true,
        // -------------------------------------------------
      };
    default:
      throw new Error();
  }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

现在每个state的转换。都是通过action的type决定的,基于上一个state和可选的payload属性返回一个新的state。例如,在请求成功的案例里,payload被用于设置新state对象的data。

总之,Reducer Hook确保状态管理的这一部分用自己的逻辑封装。通过提供action type和可选的payloads,你将总是可以预测变化。另外,你将不会再非法的state下运行。例如,先前可能搞错了设置isLoadingisError状态变成true。我们在这种情况下怎么展示呢?现在每个状态的改变都被reducer 函数变成一个合法的state对象。

在Effect Hook中阻止数据获取

在React中设置未挂载组件的状态是一个常见的问题(e.g. 由于通过React Router导航的)。我之前写过关于这个问题的文章,它描述了在各种场景中如何阻止在未挂载的组件中设置state。让我们看看怎么在我们自定义的数据获取钩子里面阻止状态设置。

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    // -------------------------------------------------
    let didCancel = false;
    // -------------------------------------------------

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);
				
        // -------------------------------------------------
        if (!didCancel) {
        // -------------------------------------------------
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        // -------------------------------------------------
        }
        // -------------------------------------------------
      } catch (error) {
        // -------------------------------------------------
        if (!didCancel) {
        // -------------------------------------------------
          dispatch({ type: 'FETCH_FAILURE' });
        // -------------------------------------------------
        }
        // -------------------------------------------------
      }
    };

    fetchData();
		
    // -------------------------------------------------
    return () => {
      didCancel = true;
    };
    // -------------------------------------------------
  }, [url]);

  return [state, setUrl];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

每个Effect Hook都有一个匹配的清除函数,它会在组件卸载的时候执行。这个清除函数是一个从hook中返回的函数。在我们的例子里,我们使用名字为didCancel的boolean类型的标志让我们数据获取逻辑知道组件状态(挂载的/未挂载的)。如果组件完成卸载,这个标志应该设置为true这个结果会阻止异步获取数据完成之后设置组件的状态 。

注:其实数据请求没有被终止 — 可以通过Axios Cancellation实现终止请求的功能—但是状态迁移在组件卸载后不会再执行。由于Axios Concellation在我看来没有更好的API,这个boolean值的标志也可以完成阻止设置state的工作。

你已经学会怎么在React获取数据的时候使用React hooks中的state和effets钩子。

如果你对在React类组件(函数式组件)里面使用render属性和高阶组件获取数据感到好奇,查看本篇文章开始处我的其他文章。除此以外,我希望这篇文章有助于你学习React Hooks和在真实世界中使用他们。


下载地址:
react与后台交互获取并渲染数据(适合初学者)一个完整的demo
在React中获取数据
51自学网,即我要自学网,自学EXCEL、自学PS、自学CAD、自学C语言、自学css3实例,是一个通过网络自主学习工作技能的自学平台,网友喜欢的软件自学网站。
京ICP备13026421号-1