相关说明:
-
对于hook相关词不翻译,感觉翻译后怪怪的。
-
effect hook 效果钩子,用于执行一些副作用例如获取数据 。
-
state hook 状态钩子。
-
使用----------- 和 ----------- 标出代码需要关注的地方。
渣翻译如下:
在这个指南中,我想给你展示使用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 setIsError
,setIsLoading
)。让我们使用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
自定义钩子的人还可以访问到 data
,isLoading
和 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下运行。例如,先前可能搞错了设置isLoading
和isError
状态变成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和在真实世界中使用他们。