經過這個教程,我想告訴你在 React 中如何使用 state 和 effect 這兩種 hooks 去請求數據。咱們將使用衆所周知的 Hacker News API 來獲取一些熱門文章。你將定義屬於你本身的數據請求的 Hooks ,而且能夠在你全部的應用中複用,也能夠發佈到 npm 。react
若是你不瞭解 React 的這些新特性,能夠查看個人另外一篇文章 introduction to React Hooks。若是你想直接查看文章的示例,能夠直接 checkout 這個 Github 倉庫。ios
注意:在 React 將來的版本中,Hooks 將不會用了獲取數據,取而代之的是一種叫作
Suspense
的東西。儘管如此,下面的方法依然是瞭解 state 和 effect 兩種 Hooks 的好方法。git
若是你沒有過在 React 中進行數據請求的經驗,能夠閱讀個人文章:How to fetch data in React。文章講解了如何使用 Class components 獲取數據,如何使用可重用的 Render Props Components 和 Higher Order Components ,以及如何進行錯誤處理和 loading 狀態。在本文中,我想用 Function components 和 React Hooks 來重現這一切。github
import React, { useState } from 'react';
function App() {
const [data, setData] = useState({ hits: [] });
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
複製代碼
App 組件將展現一個列表,列表信息來自 Hacker News articles 。狀態和狀態更新函數將經過被稱爲 useState
的狀態鉤子來生成,它負責管理經過請求獲得的 App 組件的本地狀態。初始狀態是一個空數組,目前沒有任何地方給它設置新的狀態。npm
咱們將使用 axios 來獲取數據,固然也可使用你熟悉的請求庫,或者瀏覽器自帶的 fetch API。若是你尚未安裝過 axios ,能夠經過 npm install axios
進行安裝。redux
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://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;
複製代碼
咱們在 useEffect
這個 effect hook 中,經過 axios 從 API 中獲取數據,並使用 state hook 的更新函數,將數據存入到本地 state 中。而且使用 async/await 來解析promise。axios
然而,當你運行上面的代碼的時候,你會陷入到該死的死循環中。effect hook 在組件 mount 和 update 的時候都會執行。由於咱們每次獲取數據後,都會更新 state,因此組件會更新,並再次運行 effect,這會一次又一次的請求數據。很明顯咱們須要避免這樣的bug產生,咱們只想在組件 mount 的時候請求數據。你能夠在 effect hook 提供的第二個參數中,傳入一個空數組,這樣作能夠避免組件更新的時候執行 effect hook ,可是組件在 mount 依然會執行它。api
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://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;
複製代碼
第二個參數是用來定義 hook 因此依賴的變量的。若是其中一個變量發生變化,hook 將自動運行。若是第二個參數是一個空數組,那麼 hook 將不會在組件更新是運行,由於它沒有監控任何的變量。數組
還有一個須要特別注意的點,在代碼中,咱們使用了 async/await 來獲取第三方 API 提供的數據。根據文檔,每個 async 函數都將返回一個隱式的 promise:promise
"The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result. "
「async 函數定義了一個異步函數,它返回的是一個異步函數對象,異步函數是一個經過事件循環進行操做的函數,使用隱式的 Promise 返回最終的結果。」
然而,effect hook 應該是什麼也不返回的,或者返回一個 clean up 函數的。這就是爲何你會在控制檯看到一個錯誤信息。
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
函數使用async。讓咱們來實現一個解決方案,可以在 effect hook 中使用 async 函數。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
複製代碼
這就是一個使用 React Hooks 進行數據請求的小案例。可是,若是你對錯誤處理、loading 態、如何觸發表單數據獲取以及如何複用出具處理 hook 感興趣,那咱們接着往下看。
如今咱們已經可以在組件 mount 以後獲取到數據,可是,如何使用輸入框動態告訴 API 選擇一個感興趣的話題呢?能夠看到以前的代碼,咱們默認將 "Redux" 做爲查詢參數('hn.algolia.com/api/v1/sear…'),可是咱們怎麼查詢關於 React 相關的話題呢?讓咱們實現一個 input 輸入框,能夠得到除了 「Redux」 以外的其餘的話題。如今,讓咱們爲輸入框引入一個新的 state。
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(
'http://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
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;
複製代碼
如今,請求數據和查詢參數兩個 state 相互獨立,可是咱們須要像一個辦法但願他們耦合起來,只獲取輸入框輸入的參數指定的話題文章。經過如下修改,組件應該在 mount 以後按照查詢獲取相應文章。
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, []);
return (
...
);
}
export default App;
複製代碼
實際上,咱們還缺乏部分代碼。你會發現當你在輸入框輸入內容後,並無獲取到新的數據。這是由於 useEffect 的第二個參數只是一個空數組,此時的 effect 不依賴於任何的變量,因此這隻會在 mount 只會觸發一次。可是,如今咱們須要依賴查詢條件,一旦查詢發送改變,數據請求就應該再次觸發。
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
return (
...
);
}
export default App;
複製代碼
好了,如今一旦你改變輸入框內容,數據就會從新獲取。可是如今又要另一個問題:每次輸入一個新字符,就會觸發 effect 進行一次新的請求。那麼咱們提供一個按鈕來手動觸發數據請求呢?
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(
`http://hn.algolia.com/api/v1/search?query=${search}`,
);
setData(result.data);
};
fetchData();
}, [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>
);
}
複製代碼
此外,search state 的初始狀態也是設置成了與 query state 相同的狀態,由於組件在 mount 的時候會請求一次數據,此時的結果也應該是反應的是輸入框中的搜索條件。然而, search state 和 query state 具備相似的值,這看起來比較困惑。爲何不將真實的 URL 設置到 search state 中呢?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData(result.data);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="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>
);
}
複製代碼
這就是經過 effect hook 獲取數據的案例,你能夠決定 effect 取決於哪一個 state。在這個案例中,若是 URL 的 state 發生改變,則再次運行該 effect 經過 API 從新獲取主題文章。
讓咱們在數據的加載過程當中引入一個 Loading 狀態。它只是另外一個由 state hook 管理的狀態。Loading state 用於在 App 組件中呈現 Loading 狀態。
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(
'http://hn.algolia.com/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData(result.data);
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`http://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 被修改時,調用 effect 獲取數據,Loading 狀態就會變成 true。一旦請求完成,Loading 狀態就會再次被設置爲 false。
經過 React Hooks 進行數據請求時,如何進行錯誤處理呢? 錯誤只是另外一個使用 state hook 初始化的另外一種狀態。一旦出現錯誤狀態,App 組件就能夠反饋給用戶。當使用 async/await 函數時,一般使用 try/catch 來進行錯誤捕獲,你能夠在 effect 中進行下面操做:
...
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
return (
<Fragment>
...
{isError && <div>Something went wrong ...</div>}
...
<Fragment>
);
複製代碼
effect 每次運行都會重置 error state 的狀態,這頗有用,由於每次請求失敗後,用戶可能從新嘗試,這樣就可以重置錯誤。爲了觀察代碼是否生效,你能夠填寫一個無用的 URL ,而後檢查錯誤信息是否會出現。
什麼纔是獲取數據的正確形式呢?如今咱們只有輸入框和按鈕進行組合,一旦引入更多的 input 元素,你可能想要使用表單來進行包裝。此外表單還可以觸發鍵盤的 「Enter」 事件。
function App() {
...
const doFetch = (evt) => {
evt.preventDefault();
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
}
return (
<Fragment>
<form
onSubmit={ doFetch }
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
{isError && <div>Something went wrong ...</div>}
...
</Fragment>
);
}
複製代碼
咱們能夠定義一個自定義的 hook,提取出全部與數據請求相關的東西,除了輸入框的 query state,除此以外還有 Loading 狀態、錯誤處理。還要確保返回組件中須要用到的變量。
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'http://hn.algolia.com/api/v1/search?query=redux',
);
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) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return { data, isLoading, isError, doFetch };
}
複製代碼
如今,咱們在 App 組件中使用咱們的新 hook 。
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
...
</Fragment>
);
}
複製代碼
接下來,在外部傳遞 URL 給 DoFetch
方法。
const useHackerNewsApi = () => {
...
useEffect(
...
);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment>
<form
onSubmit={event => {
doFetch(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
event.preventDefault();
}}
>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="submit">Search</button>
</form>
...
</Fragment>
);
}
複製代碼
初始的 state 也是通用的,能夠經過參數簡單的傳遞到自定義的 hook 中:
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) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useDataApi(
'http://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] },
);
return (
<Fragment>
<form
onSubmit={event => {
doFetch(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
event.preventDefault();
}}
>
<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 獲取數據的方法,hook 自己對API一無所知,它從外部獲取參數,只管理必要的 state ,如數據、 Loading 和錯誤相關的 state ,而且執行請求並將數據經過 hook 返回給組件。
目前爲止,咱們已經使用 state hooks 來管理了咱們獲取到的數據數據、Loading 狀態、錯誤狀態。然而,全部的狀態都有屬於本身的 state hook,可是他們又都鏈接在一塊兒,關心的是一樣的事情。如你所見,全部的它們都在數據獲取函數中被使用。它們一個接一個的被調用(好比:setIsError
、setIsLoading
),這纔是將它們鏈接在一塊兒的正確用法。讓咱們用一個 Reducer Hook 將這三者鏈接在一塊兒。
Reducer Hook 返回一個 state 對象和一個函數(用來改變 state 對象)。這個函數被稱爲分發函數(dispatch function),它分發一個 action,action 具備 type 和 payload 兩個屬性。全部的這些信息都在 reducer 函數中被接收,根據以前的狀態提取一個新的狀態。讓咱們看看在代碼中是如何工做的:
import React, {
Fragment,
useState,
useEffect,
useReducer,
} from 'react';
import axios from 'axios';
const dataFetchReducer = (state, action) => {
...
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
};
複製代碼
Reducer Hook 以 reducer 函數和一個初始狀態對象做爲參數。在咱們的案例中,加載的數據、Loading 狀態、錯誤狀態都是做爲初始狀態參數,且不會發生改變,可是他們被聚合到一個狀態對象中,由 reducer hook 管理,而不是單個 state hooks。
const dataFetchReducer = (state, action) => {
...
};
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]);
...
};
複製代碼
如今,在獲取數據時,可使用 dispatch 函數向 reducer 函數發送信息。使用 dispatch 函數發送的對象具備一個必填的 type
屬性和一個可選的 payload
屬性。type 屬性告訴 reducer 函數須要轉換的 state 是哪一個,還能夠從 payload 中提取新的 state。在這裏只有三個狀態轉換:初始化數據過程,通知數據請求成功的結果,以及通知數據請求失敗的結果。
在自定義 hook 的末尾,state 像之前同樣返回,可是由於咱們全部的 state 都在一個對象中,而再也不是獨立的 state ,因此 state 對象進行解構返回。這樣,調用 useDataApi
自定義 hook 的人仍然能夠 data
、isLoading
和isError
:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
複製代碼
最後咱們還缺乏 reducer 函數的實現。它須要處理三個不一樣的狀態轉換,分被稱爲 FEATCH_INIT
、FEATCH_SUCCESS
、FEATCH_FAILURE
。每一個狀態轉換都須要返回一個新的狀態。讓咱們看看使用 switch case 如何實現這個邏輯:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return { ...state };
case 'FETCH_SUCCESS':
return { ...state };
case 'FETCH_FAILURE':
return { ...state };
default:
throw new Error();
}
};
複製代碼
reducer 函數能夠經過其參數訪問當前狀態和 dispatch 傳入的 action。到目前爲止,在 switch case 語句中,每一個狀態轉換隻返回前一個狀態,析構語句用於保持 state 對象不可變(即狀態永遠不會被直接更改)。如今讓咱們重寫一些當前 state 返回的屬性,以便在每次轉換時更改 一些 state:
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();
}
};
複製代碼
如今,每一個狀態轉換(action.type決定)都返回一個基於先前 state 和可選 payload 的新狀態。例如,在請求成功的狀況下,payload 用於設置新 state 對象的 data 屬性。
總之,reducer hook 確保使用本身的邏輯封裝狀態管理的這一部分。經過提供 action type 和可選 payload ,老是會獲得可預測的狀態更改。此外,永遠不會遇到無效狀態。例如,之前可能會意外地將 isLoading
和 isError
設置爲true。在這種狀況下,UI中應該顯示什麼? 如今,由 reducer 函數定義的每一個 state 轉換都指向一個有效的 state 對象。
在React中,即便組件已經卸載,組件 state 仍然會被被賦值,這是一個常見的問題。我在以前的文章中寫過這個問題,它描述了如何防止在各類場景中爲未掛載組件設置狀態。讓咱們看看在自定義 hook 中,請求數據時如何防止設置狀態:
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]);
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
複製代碼
每一個Effect Hook都帶有一個clean up函數,它在組件卸載時運行。clean up 函數是 hook 返回的一個函數。在該案例中,咱們使用 didCancel
變量來讓 fetchData
知道組件的狀態(掛載/卸載)。若是組件確實被卸載了,則應該將標誌設置爲 true
,從而防止在最終異步解析數據獲取以後設置組件狀態。
注意:實際上並無停止數據獲取(不過能夠經過Axios取消來實現),可是再也不爲卸載的組件執行狀態轉換。因爲 Axios 取消在我看來並非最好的API,因此這個防止設置狀態的布爾標誌也能夠完成這項工做。