How to fetch data with React Hooks?

How to fetch data with React Hooks?

在這篇教程中,我將經過React Hooks中的useState、useEffect來展現如何獲取後端數據。您也能夠實現一個自定義的Hook在您應用程序中的任何地方進行復用,或者亦可單獨做爲一個獨立的node package發到npm。
css

如何使用React Hook與後端數據交互


先看一段代碼,咱們使用了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

屏幕快照 2020-05-10 下午4.10.22.png


當組件componentDidMount時候,會執行useEffect,當咱們獲取到後端數據時候咱們有更新state,同時組件更新,再次執行useEffect,周而復始。很明顯這是一個bug咱們應該避免。

事實上,咱們僅僅需求在組件掛載時候執行一次useEffect。
咱們只須要爲useEffect提供第二個參數[]來避免組件更新時候再次執行useEffect。

可是若是咱們直接在原代碼基礎上加一個[],仍然會報錯。
屏幕快照 2020-05-10 下午4.54.06.png

useEffect必須返回一個純函數或者沒有返回值,useEffect的回調必須是同步的,以此避免出現條件競爭。所以咱們能夠把async放在回調內部執行。

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

如何手動去觸發一個Hook執行?

上一個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 複製代碼

屏幕快照 2020-05-10 下午5.07.27.png

可是呢咱們發現不管如何在輸入框進行交互發現都不會再次調用API獲取數據,由於咱們忘了給useEffect傳遞第二個參數,把依賴傳入。

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的執行時機。
後端

經過React Hook處理Loading


接下來繼續優化下咱們的交互把,當咱們從後端獲取數據時候顯示一個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處理異常

當咱們使用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 複製代碼

屏幕快照 2020-05-10 下午5.43.54.png

React處理Form


接下咱們繼續寫一個常見的場景:處理表單數據。

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 複製代碼

自定義你的Hook

接下來咱們從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 。同時暴露本身的方法給外面組件調用。

經過useReducer整合數據


到目前爲止,咱們已經使用了各類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: 感興趣的能夠關注波公衆號。

相關文章
相關標籤/搜索