常规请求
import React, { useState, useEffect } from "react";import axios from "axios";function App() {const [data, setData] = useState({ hits: [] });// useEffect(async () => {// const result = await axios(// 'https://hn.algolia.com/api/v1/search?query=redux',// );// setData(result.data);// }, []);useEffect(() => {(async () => {const result = await axios("https://hn.algolia.com/api/v1/search?query=redux");setData(result.data);})();}, []);return (<ul>{data.hits.map(item => (<li key={item.objectID}><a href={item.url}>{item.title}</a></li>))}</ul>);}export default App;
7-13行中,会
index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect.. 
因为useEffect返回nothing或者clean up function,而async返回的是一个promise,如果对async或者useEffect不熟悉的参见 文章 useEffect 和 异步
第21行,如果去掉[],会发现无限循环的请求接口,这是因为请求接口之后,重新setData,导致重新渲染,重新执行该effect,如此循环……
传入一个空数组,只能在mount的时候请求一次,如果想表单某个值改变的时候出发请求,该怎么实现呢?
触发请求
import React, { Fragment, 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://hn.algolia.com/api/v1/search?query=${query}`,);setData(result.data);};fetchData();}, [query]);return (<Fragment><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><ul>{data.hits.map(item => (<li key={item.objectID}><a href={item.url}>{item.title}</a></li>))}</ul></Fragment>);}export default App;
如上,每当input输入值改变的时候,都会对应的触发请求,通过维护一个query的值作为useEffect的依赖。
每次输入哪怕是只改变一个字母,都会频繁请求,那么我们通过一个按钮来控制请求,
import React, { Fragment, useState, useEffect } from "react";import axios from "axios";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}`);setData(result.data);};fetchData();}, [search]);return (<Fragment><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><button type="button" onClick={() => setSearch(query)}>Search</button><ul>{data.hits.map(item => (<li key={item.objectID}><a href={item.url}>{item.title}</a></li>))}</ul></Fragment>);}export default App;
维护两个state,且状态同步,有点让人疑惑
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");useEffect(() => {const fetchData = async () => {const result = await axios(url);setData(result.data);};fetchData();}, [url]);return (<Fragment><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><buttontype="button"onClick={() =>setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)}>Search</button><ul>{data.hits.map(item => (<li key={item.objectID}><a href={item.url}>{item.title}</a></li>))}</ul></Fragment>);}export default App;
我们尝试封装URL,每当点击search的时候,会改变url的query,url变化,就会触发effect的执行.
Loading 和 Error Indicator
loading和error indicator
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 [loading, setLoading] = useState(false);const [err, setError] = useState(false);useEffect(() => {setLoading(true);try {const fetchData = async () => {const result = await axios(url);setData(result.data);setLoading(false);};fetchData();} catch (err) {setError(true);}}, [url]);return (<Fragment><inputtype="text"value={query}onChange={event => setQuery(event.target.value)}/><buttontype="button"onClick={() =>setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)}>Search</button>{err && <div>Something goes wrong ... </div>}{loading ? (<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;
自定义Hook
封装自定义Hooks
import React, { Fragment, useState, useEffect } from "react";import axios from "axios";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) {console.log(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><formonSubmit={event => {doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);event.preventDefault();}}><inputtype="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;
这就是自定义hook,自定义hook通常以use开头,返回数组。
hook接收参数,内部管理状态,返回data、loading、error。
useReducer中请求数据
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_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 (<Fragment><formonSubmit={event => {doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);event.preventDefault();}}><inputtype="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;
取消请求
请求api的时候,如果组件unmount,也会触发setState 详见组件unmount取消setState
在hooks中如何避免组件unmount的时候setState呢?
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];};
重点第11行、19行、23行、31-33行。
通过didCancel标志来控制。
Reference:
https://www.robinwieruch.de/react-hooks-fetch-data
