一直以来,我将useEffect看作是生命周期.直到前段时间在项目中使用useEffect出现了重复请求的问题,翻阅了相关文章让我对useEffect有了新的认识.
请求数据大概分为两种情况:第一种只在页面初始化请求数据;第二种是根据外部的变化来请求数据(比如props或者state的变化).
页面初始化请求数据
effect的第二个参数为[],表示没有依赖项,effect只会在初次渲染执行.
function SearchResults() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react',
);
setData(result.data);
}
fetchData();
}, []);
// ...
你可能会想将getFetchUrl 移到effect外部,复用逻辑
function SearchResults() {
const [data, setData] = useState({ hits: [] });
funcyion fetchData(){
async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
}
}
useEffect(() => {
fetchData();
}, []);
此时effect第二个参数设置为[]时会出现提示:遗漏了fetchData依赖。在这种情况下应该忽略对函数的依赖吗?effect不应该对它的依赖撒谎。
于是我们指定effect依赖
function SearchResults() {
const [data, setData] = useState({ hits: [] });
funcyion fetchData(){
async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
}
}
useEffect(() => {
fetchData();
}, [fetchData]);
此时,一切看起来很完美,那如果添加一个计时器会发生什么呢? 以下🌰仅仅用来测试!!!
🌰:出现了无限重复请求
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {setCount(count + 1);}, 1000);
return () => clearInterval(id);
}, [count]);
funcyion fetchData(){
async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
}
}
useEffect(() => {
fetchData();
}, [fetchData]);
count发生了变化,引起组件渲染,effect会在每次渲染后执行一次,然后在effect中更新了count状态引起渲染并再次触发effect。
你应该会问请求数据这个依赖一直没变为什么会引起effect执行请求数据的操作
一个常见的误解是,“函数从来不会改变”。这显然不是事实。实际上,在组件内定义的函数每一次渲染都在变。
当props或者state发生变化,组件会重新渲染,每一次渲染都有属于它自己的所有,包括函数,所以每次组件渲染 effect认为它所依赖的函数发现变化就会去执行对应的操作。
我们可以使用 useCallback hook包装函数
function SearchResults() {
const fetchData = useCallback(
async ()=>{
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react'
);
setData(result.data);
},[])
useEffect(() => {
fetchData();
}, [fetchData]);
}
useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。
根据外部的变化来请求数据
function SearchResults() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('react');
useEffect(() => {
const fetchData async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query='+query
);
setData(result.data);
}
fetchData();
}, [query]);
// ...
使用useCallback 将getFetchUrl 移到effects外部
function SearchResults() {
const fetchData = useCallback(async ()=>{
const result = await axios(
'https://hn.algolia.com/api/v1/search?query='+query
);
setData(result.data);
},[query]) // 指定依赖
useEffect(() => {
fetchData();
}, [fetchData]);
}
fetchData使用到了query就需要在useCallback定义依赖,query变化才执行fetchData操作.
一个完整的🌰
以下将从手动触发请求获取数据、错误处理、加载显示、如何实现可重用的数据获取自定义hooks这几个方面来实现
普通写法
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(
'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 (
<Fragment>
<form
onSubmit={() =>
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>}
{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;
自定义hooks
将属于数据获取的所有内容(属于输入字段的查询状态除外,但包括加载指示器和错误处理)放入自定义hooks中,组件无需知道相关的数据逻辑。
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 (
<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;
这是demo这就是使用自定义hooks获取数据的过程。hooks本身对 API 一无所知。它从外部接收所有参数,只管理必要的状态,例如数据、加载和错误状态。它暴露结果和操作给组件使用。
但是useDataApi还未做到状态与操作的解耦,接下来我们使用useReducer来做进一步优化
状态与操作解耦
Reducer Hook 返回一个状态对象和一个改变状态对象的函数。该函数——称为调度函数——接受一个具有类型和可选负载的动作以及状态对象的初始值。
import React, {
Fragment,
useState,
useEffect,
useReducer,
} 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_FAIL' });
}
};
fetchData();
}, [url]);
return [state, setUrl];
};
现在effect的依赖只剩下url了。相比于直接在effect里面读取所有状态,它dispatch了一个action来描述发生了什么。这使得我们的effect和isLoading、isError、data状态解耦。effect不再关心怎么更新状态,它只负责告诉我们发生了什么。更新的逻辑全都交由dataFetchReducer去统一处理:这里是完整demo
总结
- 建议将仅被effect使用的函数放到effect里面
- 如果函数作为effect依赖,那么需要在定义函数的地方用
useCallback包一层,你也可以使用useMemo来处理 - 不要盲目的忽视依赖而使用
[],你可以通过useReducer和useCallback来优化操作或者移除某些effect依赖