在這篇教程中,我將經過React Hooks中的useState、useEffect來展現如何獲取後端數據。您也能夠實現一個自定義的Hook在您應用程序中的任何地方進行復用,或者亦可單獨做爲一個獨立的node package發到npm。
css
先看一段代碼,咱們使用了useEffect hook,在它裏面經過axios獲取後端數據,而後經過setData更新數據。看起來一切很正常。node
import React, { useState, useEffect } from 'react' import axios from 'axios' import './App.css' 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) }) return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ) } export default App; 複製代碼
然而當咱們真正運行這段代碼時候發現,咱們陷入了一個無限循環。
react
import React, { useState, useEffect } from 'react' import axios from 'axios' import './App.css' function App() { const [data, setData] = useState({hits: []}) useEffect(() => { const fetchQueryData = async () => { const result = await axios( "https://hn.algolia.com/api/v1/search?query=redux" ) setData(result.data) } fetchQueryData() }, []) return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ) } export default App; 複製代碼
useEffect的第二個參數是一個數組,裏面能夠存放useEffect hook依賴的變量,若是其中某個變量發生改變,那麼hook會再次執行。若是咱們提供的是一個空數組,當組件再次更新時候hook是不會執行的,由於它並不會watch任何依賴。
ios
上一個demo咱們至關於實現一個靜默的數據獲取,即組件mount時候從後端獲取數據。那麼如今咱們再實現一個例子,經過在輸入框input中進行交互實時從後端搜索對應數據。
npm
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" function App() { const [data, setData] = useState({ hits: [] }) const [query, setQuery] = useState('redux') useEffect(() => { const fetchQueryData = async function() { const result = await axios( `https://hn.algolia.com/api/v1/search?query=${query}` ) setData(result.data) } fetchQueryData() }, []) return ( <Fragment> <input type="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 複製代碼
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" function App() { const [data, setData] = useState({ hits: [] }) const [query, setQuery] = useState('redux') useEffect(() => { const fetchQueryData = async function() { const result = await axios( `https://hn.algolia.com/api/v1/search?query=${query}` ) setData(result.data) } fetchQueryData() }, [query]) return ( <Fragment> <input type="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 複製代碼
完美!如今demo正常按照咱們的預想在執行了,接下來咱們繼續想象一個常見的場景,咱們提供一個按鈕,經過這個按鈕手動觸發搜索。
redux
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" function App() { const [data, setData] = useState({ hits: [] }) const [query, setQuery] = useState('redux') const [search, setSearch] = useState('') useEffect(() => { const fetchQueryData = async function() { const result = await axios( `https://hn.algolia.com/api/v1/search?query=${query}` ) setData(result.data) } fetchQueryData() }, [search]) return ( <Fragment> <input type="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 複製代碼
上面的代碼其實不夠優雅,由於它的query和search作的彷佛是一件事情,會讓人產生困惑,咱們能夠再次改造下。
axios
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" 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 fetchQueryData = async function() { const result = await axios(url) setData(result.data) } fetchQueryData() }, [url]) return ( <Fragment> <input type="text" value={query} onChange={(event) => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`https://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 複製代碼
可見,咱們只須要可以控制useEffect的依賴,那麼咱們就能夠控制useEffect的執行時機。
後端
接下來繼續優化下咱們的交互把,當咱們從後端獲取數據時候顯示一個Loading圖。api
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" 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) useEffect(() => { const fetchQueryData = async function() { setIsLoading(true) const result = await axios(url) setData(result.data) setIsLoading(false) } fetchQueryData() }, [url]) return ( <Fragment> <input type="text" value={query} onChange={(event) => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`) } > Search </button> {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 複製代碼
咱們發現,當組件mount時候或者url state發生變化時候,開始執行useEffect,此時isLoading設置爲true,當數據獲取完畢後,isLoading爲false。
數組
當咱們使用React Hook與後端進行數據通訊時候,若是這個過程當中發生異常,那麼咱們應該怎麼處理呢?
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" 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 fetchQueryData = async function() { setIsError(false) setIsLoading(true) try { const result = await axios(url) setData(result.data) } catch (error) { setIsError(true) } setIsLoading(false) } fetchQueryData() }, [url]) return ( <Fragment> <input type="text" value={query} onChange={(event) => setQuery(event.target.value)} /> <button type="button" onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?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> )} </Fragment> ) } export default App 複製代碼
接下咱們繼續寫一個常見的場景:處理表單數據。
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" 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 fetchQueryData = async function() { setIsError(false) setIsLoading(true) try { const result = await axios(url) setData(result.data) } catch (error) { setIsError(true) } setIsLoading(false) } fetchQueryData() }, [url]) return ( <Fragment> <form onSubmit={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`) } > <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 複製代碼
如今咱們能夠經過點擊按鈕或者輸入Enter鍵來提交數據。
可是呢如今有一個問題,當咱們點擊提交按鈕進行表單提交時候會觸發瀏覽器的默認行爲(刷新整個頁面),所以咱們須要處理下這個默認行爲。
import React, { useState, useEffect, Fragment } from "react" import axios from "axios" import "./App.css" 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 fetchQueryData = async function() { setIsError(false) setIsLoading(true) try { const result = await axios(url) setData(result.data) } catch (error) { setIsError(true) } setIsLoading(false) } fetchQueryData() }, [url]) const submitHandle = (event) => { setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`) event.preventDefault() } return ( <Fragment> <form onSubmit={(event) => submitHandle(event)} > <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 複製代碼
接下來咱們從fetching data的過程當中抽離出來實現一個自定義的hook,已實現代碼的複用。
import { useState, useEffect } from 'react' import axios from "axios" const useHackerNewsApi = (initialUrl, initialData) => { const [data, setData] = useState(initialData) const [url, setUrl] = useState(initialUrl) const [isLoading, setIsLoading] = useState(false) const [isError, setIsError] = useState(false) useEffect(() => { const fetchQueryData = async function () { setIsError(false) setIsLoading(true) try { const result = await axios(url) setData(result.data) } catch (error) { setIsError(true) } setIsLoading(false) } fetchQueryData() }, [url]) return [ { data, isLoading, isError }, setUrl ] } export default useHackerNewsApi 複製代碼
import React, { useState, Fragment } from "react" import useHackerNewsApi from './custom-hooks/hacker-news-api' import "./App.css" function App() { const [query, setQuery] = useState('redux') const [ { data, isLoading, isError }, doFetch, ] = useHackerNewsApi( "https://hn.algolia.com/api/v1/search?query=redux", {hits: []} ) const submitHandle = (event) => { doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`) event.preventDefault() } return ( <Fragment> <form onSubmit={(event) => submitHandle(event)} > <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 複製代碼
上面咱們實現了一個自定義的hook,它不須要關心調用的API的是什麼,它只須要對外接受外部的參數傳入,以及維護好本身內部的狀態如data, isLoading, isError 。同時暴露本身的方法給外面組件調用。
到目前爲止,咱們已經使用了各類state hook來管理咱們的數據,好比data, loading, error等狀態。然而,咱們能夠發現這三個狀態實際上是彼此有關聯,可是咱們確實分散的進行管理,所以咱們能夠嘗試使用useReducer對它們進行整合在一塊兒。
useReducer 返回一個狀態對象和一個能夠改變狀態對象的dispatch函數。dispatch函數接受action做爲參數,action包含type和payload屬性。
import { useState, useEffect, useReducer } from 'react' import axios from "axios" const dataFetchReducer = (state, action) => { console.log(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 useHackerNewsApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl) const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData }) useEffect(() => { const fetchQueryData = async function () { dispatch({ type: 'FETCH_INIT'}) try { const result = await axios(url) dispatch({ type: 'FETCH_SUCCESS', payload: result.data}) } catch (error) { dispatch({ type: "FETCH_FAILURE" }) } } fetchQueryData() }, [url]) return [ state, setUrl ] } export default useHackerNewsApi 複製代碼
import React, { useState, Fragment } from "react" import useHackerNewsApi from './custom-hooks/hacker-news-api' import "./App.css" function App() { const [query, setQuery] = useState('redux') const [ { data, isLoading, isError }, doFetch, ] = useHackerNewsApi( "https://hn.algolia.com/api/v1/search?query=redux", {hits: []} ) const submitHandle = (event) => { doFetch(`https://hn.algolia.com/api/v1/search?query=${query}`) event.preventDefault() } return ( <Fragment> <form onSubmit={(event) => submitHandle(event)} > <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 複製代碼
如今經過action的type決定每一個狀態的轉變,返回一個基於以前的state,以及一個可選的負載payload。例如,在一個成功的請求的狀況下,有效負載被用於設置新的狀態對象的數據。
使用React中中還會遇到一個很常見的問題是:若是在組件中發送一個請求,在請求尚未返回的時候卸載了該組件,這個時候還會嘗試設置這個狀態,會報錯。咱們須要在hooks中處理這種狀況。
有興趣的能夠看這個how to prevent setting state for unmounted components。
import { useState, useEffect, useReducer } from 'react' import axios from "axios" const dataFetchReducer = (state, action) => { console.log(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 useHackerNewsApi = (initialUrl, initialData) => { const [url, setUrl] = useState(initialUrl) const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, data: initialData }) useEffect(() => { let didCancel = false const fetchQueryData = async function () { dispatch({ type: 'FETCH_INIT'}) try { const result = await axios(url) if (!didCancel) { dispatch({ type: "FETCH_SUCCESS", payload: result.data }) } } catch (error) { if (!dispatch) { dispatch({ type: "FETCH_FAILURE" }) } } } fetchQueryData() return () => { didCancel = true } }, [url]) return [ state, setUrl ] } export default useHackerNewsApi 複製代碼
咱們能夠看到這裏新增了一個didCancel變量,若是這個變量爲true,不會再發送dispatch,也不會再執行設置狀態這個動做。這裏咱們在useEffe的返回函數中將didCancel置爲true,在卸載組件時會自動調用這段邏輯。也就避免了再卸載的組件上設置狀態。
ps: 感興趣的能夠關注波公衆號。