习惯于在class组件中执行数据获取的逻辑,当转换到函数组件使用hook来实现数据获取时,我们有点懵了,于是需要搞清楚两个问题:
- 如何实现useEffect模拟componentDidMount的生命周期?
也就是说在组件被加载后,能够去请求页面的数据。
- 如何正确使用useEffect来获取数据?
因为useEffect不仅在组件加载后会执行,在组件更新后也可能会执行,这样会导致一些问题存在。
数据状态
一般来说,当state或者props发生变化时,组件将会重新渲染。这里,将数据存放在一个状态中。
const App = () => {
const [data, setData] = useState([]);
return (
<div>
<ul>
{data.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</div>
)
}
组件挂载后获取数据
这里用到useEffect,在useEffect中执行网络请求的操作。保证在组件挂载后,能够去获取网络数据。
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
async function fetchData() {
const result = await axios('https://hn.algolia.com/api/v1/search?query=redux');
const { data = {} } = result || {};
setData(data.hits || []);
}
fetchData();
});
return (
<div>
<ul>
{data.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</div>
)
}
这时候,打开开发者工具,就会发现,一直在不断地做接口请求。这是因为useEffect的第二个数组参数。这里,我们需要记住useEffect和它的第二个参数之间的关系:
- useEffect在组件加载后,将会执行一次;
- 当useEffect的第二个参数(数组),没有传入,则在组件每次更新之后,useEffect都会执行一次;
- 当useEffect的第二个参数是一个空数组,useEffect在组件更新后,不会执行;
- 当useEffect的第二个参数不是一个空数组,useEffect在组件更新后,是否会执行,取决于该数组中的的某个元素时候是否发生了变化。如果没有一个元素发生了变化,则useEffect不会执行。
所以,在这里,我们需要给useEffect的第二个参数传入一个空数组:
useEffect(() => {}, []);
更新数据
在一个包含搜索功能的页面中,用户可通过搜索行为,来更新页面的数据。对于开发者来说,需要拿到用户键入的搜索内容,然后接口请求数据,更新页面。根据我们在class组件内的行为,我们会这么做:
- 定义一个接口请求的函数;
- 在组件挂载后,通过useEffect来执行接口请求,获取页面数据;
- 为点击搜索的行为绑定接口请求的函数。
const App = () => {
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
function searchData() {
async function fetchData() {
const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
const { data = {} } = result || {};
setData(data.hits || []);
}
fetchData();
}
useEffect(() => {
searchData();
}, []);
return (
<div>
<input value={query} onChange={(e) => {
setQuery(e.target.value);
}} />
<button onClick={searchData}>搜索</button>
<ul>
{data.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</div>
)
}
确实目前已经能够完成诉求。而整个实现过程,还是没有脱离class组件中的思维方式,通过useEffect来模式componentDidMount,同时定义行为逻辑。我们还是没有完全实现useEffect的价值,函数式组件,理应是没有副作用的,React提供useEffect来为函数组件添加副作用,在这个例子中,搜索行为触发接口请求生成了副作用,我们并没有放在useEffect,而是直接定义在函数组件中,这违背了hook作者美好的初衷,也使得组件不那么优雅。
通过useEffect来更新数据,这里还是用到了他的第二个数组参数:
const searchUrl = 'https://hn.algolia.com/api/v1/search';
const App = () => {
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
const [url, setUrl] = useState(`${searchUrl}?query=`);
useEffect(() => {
async function fetchData() {
const result = await axios(url);
const { data = {} } = result || {};
setData(data.hits || []);
}
fetchData();
}, [url]);
return (
<div>
<input value={query} onChange={(e) => {
setQuery(e.target.value);
}} />
<button onClick={() => {
setUrl(`${searchUrl}?query=${query}`)
}}>搜索</button>
<ul>
{data.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</div>
)
}
该例子中,接口请求依赖于接口url,当url发生变化时,则触发useEffect的执行。这样的编写方式,将所有的副作用都放在了useEffect中,组件本身只用维持组件内部的状态:data、query、url。实现了在组件加载时接口请求,在组件的url状态发生了后,更新页面。相比于上一种方式,更加优雅,更加具有函数式组件的特色。
自定义hook,抽离逻辑
实际的开发中,在接口请求时,需要一个loading状态,当接口请求发生了问题时,需要有一个错误状态,一般来说,可以这么写:
const searchUrl = 'https://hn.algolia.com/api/v1/search';
const App = () => {
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
const [url, setUrl] = useState(`${searchUrl}?query=`);
const [isLoading, setLoading] = useState(false);
const [isError, setError] = useState(false);
useEffect(() => {
setLoading(true);
async function fetchData() {
const result = await axios(url);
const { data = {}, error } = result || {};
if (error) {
setError(true)
} else {
setData(data.hits || []);
}
setLoading(false);
}
fetchData();
}, [url]);
return (
<div>
<input value={query} onChange={(e) => {
setQuery(e.target.value);
}} />
<button onClick={() => {
setUrl(`${searchUrl}?query=${query}`)
}}>搜索</button>
{isError && <p>ERROR</p>}
{isLoading ? (<p>loading....</p>) : (
<ul>
{data.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</div>
)
}
在这个页面中,维持了data、query、url、isLoading、isError这个五个状态,并且所有的逻辑都写在了这个组件中,组件主要的旋律是渲染内容,这些庞大的逻辑使得组件显得臃肿,而且类似该页面的的逻辑,在别的页面中也有,我们完全可以将这些关于接口请求的逻辑抽离出来,为函数组件减重,不仅使得代码清爽,同时能够复用逻辑。通过自定义hook,如下:
const searchUrl = 'https://hn.algolia.com/api/v1/search';
// 自定义hook
const useNetWork = (initData = [], initUrl) => {
const [data, setData] = useState(initData);
const [url, setUrl] = useState(initUrl);
const [isLoading, setLoading] = useState(false);
const [isError, setError] = useState(false);
useEffect(() => {
setLoading(true);
async function fetchData() {
const result = await axios(url);
const { data = {}, error } = result || {};
if (error) {
setError(true)
} else {
setData(data.hits || []);
}
setLoading(false);
}
fetchData();
}, [url]);
return [{data, isLoading, isError}, setUrl];
}
// 函数式组件
const App = () => {
const [query, setQuery] = useState('');
const [{ data, isLoading, isError }, setUrl] = useNetWork([], searchUrl);
return (
<div>....</div>
)
}
使用useReducer
到上面一小节为止,已经很好地使用了useEffect hook和自定义hook来为函数式组件添加副作用,并且抽离逻辑,减轻组件体重,实现组件诉求。在整个整个过程中,自定义hook提供的data、isLoading、isError这几个状态来相互作用、相互影响,拧在一起,决定了函数组件的展示逻辑。其实,这四个状态的值是可预测的,并且是仅仅绑定在一起的,类似于redux,可通过dispatch行为,更改这些状态,实现数据流管理。react提供了useReducer,将这几个状态集合在一起,进行管理。
const searchUrl = 'https://hn.algolia.com/api/v1/search';
function dataReducer(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();
}
}
// 自定义hook
const useNetWork = (initData = [], initUrl) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer(dataReducer, {
isLoading: false,
isError: false,
data: initData,
});
useEffect(() => {
dispatch({ type: 'FETCH_INIT' });
async function fetchData() {
try {
const result = await axios(url);
const { data = {} } = result || {};
dispatch({ type: 'FETCH_SUCCESS', payload: data.hits || [] });
} catch (error) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
fetchData();
}, [url]);
return [{data, isLoading, isError}, setUrl];
}
// 函数式组件
const App = () => {
const [query, setQuery] = useState('');
const [{ data, isLoading, isError }, setUrl] = useNetWork([], searchUrl);
return (
<div>....</div>
)
}
通过useReducer,将状态的管理抽离出来,自定义hoo中,主要逻辑处理,发布状态,reducer对状态进行更新。
在useEffect中取消数据的获取
在日常的使用中,接口还在进行网络请求时,组件已经处于卸载状态,但是当接口返回后,仍然对组件的状态进行更改,我们可以通过useEffect的请求函数,来避免这种情况:
// 自定义hook
const useNetWork = (initData = [], initUrl) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer(dataReducer, {
isLoading: false,
isError: false,
data: initData,
});
useEffect(() => {
let canCancel = false;
dispatch({ type: 'FETCH_INIT' });
async function fetchData() {
try {
const result = await axios(url);
const { data = {} } = result || {};
if (!cancel) {
dispatch({ type: 'FETCH_SUCCESS', payload: data.hits || [] });
}
} catch (error) {
if (!cancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
}
fetchData();
return () => {
canCancel = true;
}
}, [url]);
return [{data, isLoading, isError}, setUrl];
}
在useEffect中定义一个canCancel变量,并且返回更改canCancel为true的清理函数,在组件卸载后,canCancel标志为true,在接口返回后,无法组件的状态进行更改。
在我们的实操中,可以看到,当每次url发生变化后,清理函数就会执行,旧的url的对应的canCancel就会为true,旧的url对应的接口返回,就不会更新组件的状态。每一轮useEffect对应的数据都是那一轮的,不会影响到下一轮,所以,当上一轮useEffect的canCancel变为true,并不会影响到下一轮,因为下一轮useEffect有一个新的canCancel。关于每一轮useEffect的数据问题,可以到下一篇文章再做一个详细讲解,这是一个很有意思的问题。