翻译自 How to fetch data with React Hooks, 不好意思, 翻译的比较差, 主要看代码. 我也是英语初学者, 主要是理解 代码预览
在这个教程, 我想要向您展示如何使用State 和 Effect钩子在 React Hook来获取数据, 我们将使用广为人知的 Hacker News API 从科技界获取热门文章, 您还将实现数据获取的自定义钩子可以在任何地方重用.
用React Hook 获取数据
如何你不熟悉在React中如何获取数据,在本文中, 我想向您展示函数组件中的React Hook 的所有内容
import React from 'react'
import { useState } from 'react'
const App = () => {
const [data, setDate] = useState({ hits: [] })
return (
{data.hits.map((item) => (
-
{item.title}
))}
)
}
export default App
这个App组件展示了list 的每一个 item(hits = Hacker News articles), 这个state 和 updateState函数来自被叫做 useState
的钩子, 它负责管理我们将为 App 组件数据获取的本地状态. 初始的 state 是一个表示对象的 hits 空列表, 没有人为此设置任何 state
我们将使用 axios
来获取数据, 你可以通过 npm install axios
来安装依赖, 然后实现 effect 钩子获取数据
import * as React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
"https://hn.algolia.com/api/v1/search?query=redux"
);
setData(result.data);
};
fetchData();
});
return (
{data.hits.map((item) => (
-
{item.title}
))}
);
}
export default App;
上面叫做 useEffect
的钩子被用于从 axios
获取数据, 并用 setData
函数来更新数据设置组件的当前状态, Promise
使用 async/await
实现
然而, 当你运行你的程序, 你就会陷入一个恶心循环, effect
钩子在组件挂载时运行也在组件更新时运行, 因为我们每次获取数据之后都要设置状态, 所有组件会更新, effect
会再次运行, 会一次次的请求数据, 这是一个 bug, 需要我们避免. 我们只想在组件挂载时获取数据
import * as React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
"https://hn.algolia.com/api/v1/search?query=redux"
);
setData(result.data);
};
fetchData();
}, []);
return (
{data.hits.map((item) => (
-
{item.title}
))}
);
}
export default App;
第二个参数可以用来定义 useEffect
钩子依赖的所有的变量(在数组中分配), 如果其中一个变量改变, useEffect
会再次运行, 如果数组的变量是空的, 更新组件时根本不会运行, 因为它不需要观察任何变量
如何手动/以程序触发一个钩子
很好, 一旦组件挂载, 我们就可以获取数据了, 但是如何通过使用一个 input
来告诉 API
我们对哪个主题感兴趣呢? query=redux
是一个默认的查询, 但是关于 React
的主题呢? 让我们实现一个 input 元素, 能够让别人来获取除了 Redux
之外的主题, 因此, 为输入框引入一个新的状态
import * as React from 'react';
import { useState, useEffect, Fragment } 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(
'https://hn.algolia.com/api/v1/search?query=redux'
);
setData(result.data);
};
fetchData();
}, []);
return (
setQuery(event.target.value)}
/>
{data.hits.map((item) => (
-
{item.title}
))}
);
}
export default App;
目前,两个 state
彼此之间是互相独立的, 但现在你希望他们耦合到只需要输入字段查询指定的文章, 所以组件一旦挂载, 就应该按照查询条件获取所有文章
// ...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}` // 动态参数
);
setData(result.data);
};
fetchData();
}, []);
return (
// ...
);
}
少了一点: 当你尝试输入一些值在 input
中, 没有其他数据获取后, 触发 effect
的效果, 这是因为你提供了 空数组作为第二个参数
, 这种效果不依赖于任何变量, 因此只在组件挂载时才会触发, 然而, 现在这个渲染效果依赖 query
的变化, 数据请求应该再次触发
// ...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`
);
setData(result.data);
};
fetchData();
}, [query]); // 添加 query 依赖
return (
// ...
);
}
一旦你更改了输入框的值, 就会重新请求数据, 但是这会产生另外一个问题: 在每一个字符输入到输入框中, 就会产生一次请求. 如何 提供一个按钮
来触发请求, 手动触发钩子?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`
);
setData(result.data);
};
fetchData();
}, [query]);
return (
setQuery(event.target.value)}
/>
{data.hits.map((item) => (
-
{item.title}
))}
);
}
现在, 让 effect
依赖于 search state
, 而不是随着字符的变化的每一次查询, 一旦用户点击 button, 新的搜索状态应该手动触发 effect
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(
`https://hn.algolia.com/api/v1/search?query=${search}` // 改变 search
);
setData(result.data);
};
fetchData();
}, [search]); // 切换依赖
return (
// ...
);
}
搜索状态的初始状态也被设置为和查询状态相同的状态, 因为组件也在挂载时获取数据, 因此结果应该显示在输入框内, 具有形似的查询和搜索状态令人有点困惑, 为什么我们不设置一个实际的 URL 替代 search state
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux'
); // 替换search
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData(result.data);
};
fetchData();
}, [url]); // 更换依赖项
return (
setQuery(event.target.value)}
/>
{data.hits.map((item) => (
-
{item.title}
))}
);
}
你可以决定 effect
效果 哪一个 state
依赖, 一旦在点击或其他副作用中设置此状态, 这个effect会再次渲染, 在上面代码的情况下, 如果 url 变化, 会再次请求 api
给 Hook 加上 Loading
让我们为数据获取加入一个 loading
, 这仅仅是加一个新的 state, 由 state hook
管理,
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux'
);
const [isLoading, setIsLoading] = useState(false); // 添加loading
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
setQuery(event.target.value)}
/>
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
上面代码中, 一旦开始数据请求, loading = true
, 数据请求完成, laoding = false
错误处理
如何使用钩子对数据获取的错误处理? 这个错误是另一个状态初始化的 state hook
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'https://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 (
setQuery(event.target.value)}
/>
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
使用 form 来获取数据
如何正确的使用表单来获取数据? 到目前为止, 我们只是组合 input
和 button
, 一旦你引入更多的 input 元素, 你也许想要使用 form 元素来包裹它们
function App() {
// ...
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
但是现在点击 submit button
浏览器会重新加载, 因为这是提交表单的浏览器原生行为, 为了阻止默认行为, 我们可以调用React事件
function App() {
// ...
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
现在当你点击按钮时, 浏览器不会重新加载了, 它和以前一样工作, 但是现在是 form 而不是纯粹的 input 和 button 组合了, 你也可以用键盘上的回车键
自定以数据获取钩子
为了提取一个请求数据的自定义hook, 移动属于数据获取的所有数据, 除了属于 input 的查询字段, 但是包含 loading/error state
至自己的函数, 同样确保 return 所有的变量在 App组件中使用过的函数
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
"https://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];
};
现在, 你可以在 App 组件中使用了
function App() {
const [query, setQuery] = useState("redux");
const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
初始状态也是通用的, 将它简单传递到新的钩子, 设置两个参数
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("https://hn.algolia.com/api/v1/search?query=redux", {
hits: []
});
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
这就是用自定义钩子获取数据的过程, 钩子本身对 API 一无所知, 它从外部接受所有参数并且只管理必要的状态例如: data loading error
Reducer Hook 数据请求
到目前为止, 我们使用不同的状态钩子来管理数据的数据获取状态, 如你所见, 他们都在数据获取函数中使用, 让我们用一个 Reducer Hook
来把他们组合起来
一个 Reducer Hook
返回给我们一个状态对象和一个修改状态对象的函数 function -> dispatch function -> 一个type 和一个可选的 payload
, 我们看一下 reducer
是如何工作的.
const dataFetchReducer = (state, action) => {
// ...
}
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData
});
// ...
};
Reducer Hook
需要 reducer
函数和一个初始化的state对象作为参数, 在我们的代码中, 它们被 reducer 一起管理, 而不是 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]);
};
现在, 当请求数据时, dispatch 函数
可以发送信息到 reducer function
, 使用 dispatch
派发具有 type
类型和 可选的payload
属性, type类型告诉 reducer
需要应用那个状态转换, 并且 reducer 可以使用 payload来提取新的状态, 我们只有三个状态转换
在自定义hook的最后, 状态和以前一样返回, 我们有一个状态对象而不是一个独立的状态, 来完善 reducer
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();
}
};
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]);
return [state, setUrl];
};
现在每个状态的过渡, 由 type 的类型决定, 根据以前的状态和可选的载荷返回一个新状态
最后, Reducer Hook
确保状态管理封装了自己的逻辑, 通过提供操作类型和可选的 payload, 你总是会得到一个可预测的变化, 另外, 你永远不会进入无效状态, 看一下最终代码
import * as React from "react";
import { useState, useEffect, useReducer, Fragment } from "react";
import axios from "axios";
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();
}
};
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]);
return [state, setUrl];
};
function App() {
const [query, setQuery] = useState("redux");
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://hn.algolia.com/api/v1/search?query=redux",
{
hits: []
}
);
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
export default App;
终止数据请求在 effect hook
中
React中的一个常见问题是即使组件已经卸载, 组件状态也会被设置, 让我们看看如何放置在自定义钩子中设置状态以获取数据
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];
};
每一个 effect hook
都带有清理功能, 该功能在组件卸载时运行, 清理函数是从钩子的 return 一个函数, 在我们的例子中, 我们使用一个叫做 didCancel
的 boolean
值来让我们的数据获取逻辑了解组件的状态(mounted/unmounted)
Note: 实际上不是数据获取被中止 -- 这可以通过 Axios Cancellation 实现 -- 但是对于 unmounted
组件不在执行状态转换, 由于 axios cancel
在我看来并不是最好的 API, 所以设置这个 boolean
也可以完成这个工作
你已经了解了如何在 React
中使用 state 和 effect
的钩子来获取数据, 谢谢, 最终代码贴一下
import * as React from "react";
import { useState, useEffect, useReducer, Fragment } from "react";
import axios from "axios";
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();
}
};
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];
};
function App() {
const [query, setQuery] = useState("redux");
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://hn.algolia.com/api/v1/search?query=redux",
{
hits: []
}
);
return (
{isError && Something went wrong ...}
{isLoading ? (
Loading....
) : (
{data.hits.map((item) => (
-
{item.title}
))}
)}
);
}
export default App;