在这篇教程中,我将经过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: 感兴趣的能够关注波公众号。