封装请求需要考虑到的点:请求发起与监听、错误处理、loading spinners。
简易请求
简易的请求
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://example.com/api',
);
setData(result.data);
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
对 useEffect
的第二个参数添加依赖项之后,只有当依赖发生变更之后,useEffect
才会再次执行。现在我们输入的参数为空,那就只有在组件首次创建的时候才会执行。
在 useEffect
的第一个参数中使用 async 函数是不被允许的,但我们可以在其内部进行定义。
function App () {
// ...
useEffect(() => {
const fetchData = async () => {
const result = await axios('https://example.com/api')
setData(result.data)
}
fetchData()
}, [])
// ...
}
手动添加依赖
如果想手动触发请求,这时我们可以为 useEffect
来添加实际的依赖,然后手动触发对这个依赖进行修改,进而触发实际请求。
import React, { 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(
`https://example.com/api?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>
);
}
export default App;
我们将请求的参数 query
作为依赖进行添加,这样当这个参数发生改变的时候,useEffect
就可以再次触发,从而发起新的请求。
添加触发按钮
进一步,我们可以添加一个手动触发的按钮。
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://example.com/api?query=redux',
);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData(result.data);
};
fetchData();
}, [url]);
return (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://example.com/api?query=${query}`)
}
>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</>
);
}
我们新增一个 state,用来存放 query 的值,当点击按钮的时候手动对其进行赋值,进而触发 useEffect
的再次执行。相当于是间接对依赖项进行修改,从而触发请求。
添加 loading 和请求状态
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://example.com/api?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://example.com/api?query=${query}`)
}
>
Search
</button>
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</Fragment>
);
}
export default App;
这里我们新增了一个 loading 状态,根据请求的阶段对其进行操作,从而来标明当前的状态。
添加错误处理
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://example.com/api?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 (
<>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://example.com/api?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>
)}
</>
);
}
export default App;
这里我们新增了一个 isError
的 state 来表明请求的状态,并且增加了错误处理。
自定义请求 hook
我们可以将关于请求的部分进行抽离,做成一个单独的 hook 函数。
const useFetch = (initUrl, initData) => {
const [data, setData] = useState(initData);
const [url, setUrl] = useState(initUrl);
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] = useFetch()
return (
<>
<form onSubmit={evt => {
doFetch(`http://example.com/api?query=${query}`)
evt.preventDefault()
}}>
<input type="text"
value={query}
onChaneg={evt => setQuery(evt.target.value)}
/>
<button type="submit">Search</button>
</form>
{ isError && <div>出错了。。。</div> }
{
isLoading ? (<div>加载中。。。</div>)
: (
<ul>
{data.hits.map(item => (
<li key={item.id}>
<a href={item.url}>{ item.title }</a>
</li>
))}
</ul>
)
}
</>
)
}
这样我们就完成了逻辑的抽离,将请求函数进行单独封装,而不需要使用往常的高阶组件来对组件类上面添加额外的方法。
使用 useReducer
在上面的例子中,我们使用了多个自定义的 state 来处理请求。这些独立的 state 都是为了同一个目的而存在。我们可以使用 useReducer
将它们统一起来进行处理。这里相当于我们在局部创建了一个 redux。
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()
}
}
这里我们创建了一个 reducer,将请求分为不同的状态,从而将多个独立的 state 统一起来。提高处理的效率。
const useFetch = (initUrl, initData) => {
const [url, setUrl] = useState(initUrl)
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initData
})
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' })
try {
const result = await axios(url)
dispatch({ type: 'FETCH_SUCCESS', payload: result.data })
} catch (err) {
dispatch({ type: 'FETCH_FAILURE' })
}
}
})
return [state, setUrl]
}
添加取消请求的操作
const useFetch = (initUrl, initData) => {
const [url, setUrl] = useState(initUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initData,
});
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];
};
useEffect
可以返回一个清除函数,它会在组件 unmount 的时候执行。因为我们的请求是异步操作,会在第17行 await
一段时间,如果在请求结果返回之前 unmout。那么 didCancel 就会变成 true,当结果返回时就不会去执行 FETCH_SUCCESS 或者 FETCH_FAILURE。