How to fetch data with React Hooks?
在这篇教程中,我将通过React Hooks中的useState、useEffect来展示如何获取后端数据。您也可以实现一个自定义的Hook在您应用程序中的任何地方进行复用,或者亦可单独作为一个独立的node package发到npm。
如何使用React Hook与后端数据交互
先看一段代码,我们使用了useEffect hook,在它里面通过axios获取后端数据,然后通过setData更新数据。看起来一切很正常。
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;
然而当我们真正运行这段代码时候发现,我们陷入了一个无限循环。
当组件componentDidMount时候,会执行useEffect,当我们获取到后端数据时候我们有更新state,同时组件更新,再次执行useEffect,周而复始。很明显这是一个bug我们应该避免。
事实上,我们仅仅需求在组件挂载时候执行一次useEffect。
我们只需要为useEffect提供第二个参数[]来避免组件更新时候再次执行useEffect。
但是如果我们直接在原代码基础上加一个[],仍然会报错。
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任何依赖。
如何手动去触发一个Hook执行?
上一个demo我们相当于实现一个静默的数据获取,即组件mount时候从后端获取数据。那么现在我们再实现一个例子,通过在输入框input中进行交互实时从后端搜索对应数据。
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
但是呢我们发现无论如何在输入框进行交互发现都不会再次调用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正常按照我们的预想在执行了,接下来我们继续想象一个常见的场景,我们提供一个按钮,通过这个按钮手动触发搜索。
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做的似乎是一件事情,会让人产生困惑,我们可以再次改造下。
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图。
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
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: 感兴趣的可以关注波公众号。