原文連接: www.robinwieruch.de/react-hooks…javascript
在本教程中,我想經過state和effect hook來像你展現如何用React Hooks來獲取數據。我將會使用Hacker News的API來獲取熱門的技術文章。你將會實現一個屬於你本身的自定義hook來在你程序的任何地方複用,或者是做爲一個npm包發佈出來。java
若是你還不知道這個React的新特性,那麼點擊React Hooks介紹,若是你想直接查看最後的實現效果,請點擊這個github倉庫。react
注意:在將來,React Hooks將不會用於React的數據獲取,一個叫作Suspense的特性將會去負責它。但下面的教程仍會讓你去更多的瞭解關於React中的state和effect hook。ios
若是你對在React中獲取數據還不熟悉,能夠查看我其餘的React獲取數據的文章。它將會引導你經過使用React的class組件來獲取數據,而且還能夠和render props或者高階組件一塊兒使用,以及結合錯誤處理和加載狀態。在這篇文章中,我將會在function組件中使用React Hooks來展現這些功能。git
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組件展現了一個包含不少項的list(hits = Hacker News 文章)。state和state的更新函數來自於state hook中useState的調用,它負責管理咱們用來渲染list數據的本地狀態,初始狀態是一個空數組,此時尚未爲其設置任何的狀態。github
咱們將使用axios來獲取數據,固然你也可使用其餘的庫或者fetch API,若是你還沒安裝axios,你能夠在命令行使用npm install axios
來安裝它。而後來實現用於數據獲取的effect hook:npm
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;
複製代碼
經過axios在useEffect中獲取數據,而後經過setData將數據放到組件本地的state中,並經過async/await來處理Promise。編程
然而當你運行程序的時候,你應該會遇到一個討厭的循環。effect hook不只在組件mount的時候也會在update的時候運行。由於咱們在每一次的數據獲取以後,會去經過setState設置狀態,這時候組件update而後effect就會運行一遍,這就形成了數據一次又一次的獲取。咱們僅僅是想要在組件mount的時候來獲取一次數據,這就是爲何咱們須要在useEffect的第二個參數提供一個空數組,從而實現只在mount的時候觸發數據獲取而不是每一次update。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;
複製代碼
第二個參數能夠定義hooks所依賴的變量(在一個數組中去分配),若是一個變量改變了,hooks將會執行一次,若是是一個空數組的話,hooks將不會在組件更新的時候執行,由於它沒有監聽到任何的變量。axios
這裏還有一個陷阱,在代碼中,咱們使用async/await從第三方的API中獲取數據,根據文檔,每個async函數都將返回一個promise,async函數聲明定義了一個異步函數,它返回一個asyncFunction對象,異步函數是經過事件循環異步操做的函數,使用隱式Promise返回其結果。可是,effect hook應該不返回任何內容或清除功能,這就是爲何你會在控制檯看到如下警告:07:41:22.910 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內部使用異步函數來實現它的解決方案。
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獲取數據。可是,若是你對錯誤處理、加載提示、如何從表單中觸發數據獲取以及如何實現可重用的數據獲取hook感興趣,請繼續閱讀。
好的,咱們在mount後獲取了一次數據,可是,若是使用input的字段來告訴API哪個話題是咱們感興趣的呢?「Redux」能夠做爲咱們的默認查詢,若是是關於「React」的呢?讓咱們實現一個input元素,使某人可以獲取「Redux」之外的話題。所以,爲input元素引入一個新的狀態。
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; 複製代碼
目前,這兩個狀態彼此獨立,但如今但願將它們耦合起來,以獲取由input中的輸入來查詢指定的項目。經過下面的更改,組件應該在掛載以後經過查詢詞獲取全部數據。
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;
複製代碼
還差一部分:當你嘗試在input中輸入一些內容時,在mount以後就不會再獲取任何數據了,這是由於咱們提供了空數組做爲第二個參數,effect沒有依賴任何變量,所以只會在mount的時候觸發,可是如今的effect應該依賴query,每當query改變的時候,就應該觸發數據的獲取。
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;
複製代碼
如今每當input的值更新的時候就能夠從新獲取數據了。但這又致使了另外一個問題:對於input中鍵入的每一個字符,都會觸發該效果,並執行一個數據提取請求。如何提供一個按鈕來觸發請求,從而手動hook呢?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
const fetchData = async () => {
const result = await axios(
`http://hn.algolia.com/api/v1/search?query=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
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> ); } 複製代碼
如今,effect依賴於於search,而不是隨輸入字段中變化的query。一旦用戶點擊按鈕,新的search就會被設置,而且應該手動觸發effect hook。
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 (
...
);
}
export default App;
複製代碼
此外,search的初始值也設置爲與query相同,由於組件也在mount時獲取數據,所以結果應反映輸入字段中的值。可是,具備相似的query和search狀態有點使人困惑。爲何不將實際的URL設置爲狀態而來代替search?
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依賴於哪一個狀態。一旦在點擊或其餘effect中設置此狀態,此effect將再次運行。在這種狀況下,若是URL狀態發生變化,effect將再次運行以從API獲取數據。
讓咱們爲數據獲取引入一個加載提示。它只是另外一個由state hook管理的狀態。loading被用於在組件中渲染一個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; 複製代碼
一旦調用該effect進行數據獲取(當組件mount或URL狀態更改時發生),加載狀態將設置爲true。一旦請求完成,加載狀態將再次設置爲false。
若是在React Hooks中加上錯誤處理呢,錯誤只是用state hook初始化的另外一個狀態。一旦出現錯誤狀態,應用程序組件就能夠爲用戶提供反饋。使用async/await時,一般使用try/catch塊進行錯誤處理。你能夠在effect內作到:
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);
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> <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> {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; 複製代碼
到目前爲止,咱們只有input和按鈕的組合。一旦引入更多的輸入元素,您可能須要用一個表單元素包裝它們。此外,表單還能夠經過鍵盤上的「enter」來觸發。
function App() {
...
return (
<Fragment> <form onSubmit={() => setUrl(`http://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>} ... </Fragment> ); } 複製代碼
可是如今瀏覽器在單擊提交按鈕時頁面會從新加載,由於這是瀏覽器在提交表單時的固有行爲。爲了防止默認行爲,咱們能夠經過event.preventDefault()取消默認行爲。這也是在React類組件中實現的方法。
function App() {
...
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return (
<Fragment> <form onSubmit={event => { doFetch(); 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>} ... </Fragment> ); } 複製代碼
如今,當你單擊提交按鈕時,瀏覽器不會再從新加載。它和之前同樣工做,但此次使用的是表單,而不是簡單的input和按鈕組合。你也能夠按鍵盤上的「回車」鍵。
爲了提取用於數據獲取的自定義hook,請將屬於數據獲取的全部內容,移動到一個本身的函數中。還要確保可以返回App組件所須要的所有變量。
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>
);
}
複製代碼
接下來,從dofetch函數外部傳遞URL狀態:
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> ); } 複製代碼
初始狀態也能夠變爲通用狀態。把它簡單地傳遞給新的自定義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一無所知。它從外部接收全部參數,只管理必要的狀態,如數據、加載和錯誤狀態。它執行請求並將數據做爲自定義數據獲取hook返回給組件。
reducer hook返回一個狀態對象和一個改變狀態對象的函數。dispatch函數接收type和可選的payload。全部這些信息都在實際的reducer函數中使用,從之前的狀態、包含可選payload和type的action中提取新的狀態。讓咱們看看這在代碼中是如何工做的:
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函數和一個初始化的狀態對象做爲參數,在咱們的例子中,數據、加載和錯誤狀態的初始狀態的參數沒有改變,可是它們被聚合到由一個reducer hook管理的一個狀態對象,而不是單個state hook。
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函數須要應用哪一個狀態轉換,而且Reducer還可使用payload來提取新狀態。畢竟,咱們只有三種狀態轉換:初始化獲取過程,通知成功的數據獲取結果,以及通知錯誤的數據獲取結果。
在自定義hook的最後,狀態像之前同樣返回,可是由於咱們有一個狀態對象,而再也不是獨立狀態,因此須要用擴展運算符返回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函數的實現。它須要處理三種不一樣的狀態轉換,即FETCH_INIT、FETCH_SUCCESS和FETCH_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函數能夠經過其參數訪問當前狀態和action。到目前爲止,switch case語句中的每一個狀態轉換隻會返回原來的狀態。...
語句用於保持狀態對象不變(意味着狀態永遠不會直接改變),如今,讓咱們重寫一些當前狀態返回的屬性,以便在每次狀態轉換時更改狀態:
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();
}
};
複製代碼
如今,每一個狀態轉換(由操做的type決定)都將基於先前的狀態和可選的payload返回一個新的狀態。例如,在成功請求的狀況下,payload用於設置新狀態對象的數據。
總之,reducer hook確保狀態管理的這一部分是用本身的邏輯封裝的。經過提供type和可選payload,你將始終已一個可預測的狀態結束。此外,你將永遠不會進入無效狀態。例如,之前可能會意外地將isloading和isError狀態設置爲true。在這個案例的用戶界面中應該顯示什麼?如今,reducer函數定義的每一個狀態轉換都會致使一個有效的狀態對象。
即便組件已經卸載(例如,因爲使用react路由器導航而離開),設置組件狀態也是react中的一個常見問題。我之前在這裏寫過這個問題,它描述瞭如何防止在各類場景中爲unmount的組件設置狀態。讓咱們看看如何防止在自定義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功能,在組件卸載時運行。clean函數是從hook返回的一個函數。在咱們的例子中,咱們使用一個名爲didCancel的布爾標誌,讓咱們的數據獲取邏輯知道組件的狀態(已裝載/未裝載)。若是組件已卸載,則標誌應設置爲「tree」,這將致使在最終異步解決數據提取後沒法設置組件狀態。
注意:事實上,數據獲取不會停止——這能夠經過axios的Cancellation實現——可是對於未安裝的組件,狀態轉換會再也不執行。由於在我看來,axios的Cancellation並非最好的API,因此這個防止設置狀態的布爾標誌也能起到做用。
你已經瞭解了在React中state和effect hook如何用於獲取數據。若是您對使用render props和高階組件在類組件(和函數組件)中獲取數據很感興趣,請從一開始就去個人另外一篇文章。不然,我但願本文對您瞭解react hook以及如何在現實場景中使用它們很是有用。