封装请求需要考虑到的点:请求发起与监听、错误处理、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 (<><inputtype="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 (<><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><buttontype="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><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><buttontype="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 (<><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><buttontype="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。
