原文:www.robinwieruch.de/react-hooks…javascript
原做者:Robin Wieruchhtml
在本文中,我將會向你展現在React中怎樣用Hooks來獲取數據經過使用state和effect hooks。咱們將用衆所周知的Hacker News API來獲取科技界的熱門文章。你也能夠實現獲取數據的自定義hook,在應用的任何位置複用,也能夠做爲獨立的依賴包在npm上發佈。java
若是你對這個React的新功能一無所知,請查看個人另外一篇文章 introduction to React Hooks。若是你想查看直接查看文章的示例,請查看此Github倉庫。react
提示:在未來的版本中,React Hooks不適用於在React中獲取數據。取而代之的是一個叫作Suspense
的功能。儘管如此,下面的練習依然是瞭解 state 和 effect 兩種 Hooks 的好方法。ios
若是你不熟悉在React中獲取數據,能夠閱讀個人文章:How to fetch data in React。文章將講解如何使用class components獲取數據,如何複用Render Prop Components和Higher-Order Components,以及如何進行錯誤處理和 loading 狀態。在本文中,我會在function components中使用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 組件將展現一個列表,hits列表信息來自 Hacker News articles 。狀態和狀態更新函數將經過 useState
的hook來生成,它將負責管理hits列表數據的本地狀態。初始狀態是一個空數組,此時尚未爲其設置任何的狀態。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調用API在useEffect
這個 effect hook中獲取數據,而後經過setData將數據放到組件本地的state中,而後經過async/await來處理Promise。redux
可是,當你運行應用程序時,你會遇到一個討厭的循環。由於effect hook不只在組件掛載是執行,在組件更新過程當中也會執行。由於咱們在每一次的數據獲取後都會從新設置狀態,這時候組件update而後effect hook就會從新運行一遍,這就形成了數據一次又一次的獲取。咱們只想在組件掛載階段時獲取數據。這就是你要給effect hook的第二個參數傳入一個空數組的緣由,這樣作能夠避免組件更新階段執行 effect hook ,可是依然會在掛載階段執行它。axios
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所依賴的所有變量(存放在數組中),若是一個變量改變了,effect hook就會被執行一次,若是是一個空數組的話,hooks將不會在組件更新的時候執行,由於它沒有監聽到任何的變量。
還有最後一個問題,在代碼中,咱們使用了 async/await 來獲取第三方 API 提供的數據。根據文檔,每個 async 函數都將返回一個隱式的 promise:「async 函數定義了一個異步函數,它返回的是一個AsyncFunction
對象,異步函數是一個經過事件循環進行操做的函數,使用隱式的 Promise 返回最終的結果。」。
可是,effect hook應該不返回任何內容或返回一個clean up函數,這就是爲何你會在控制檯看到如下警告: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 ...
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 ...;
}
export default App;
複製代碼
這就是用React Hooks獲取數據的小🌰。可是,若是你對錯誤處理、loading狀態、如何從表單中觸發數據獲取以及如何複用數據獲取的hook感興趣,請繼續閱讀。
好的,咱們在組件掛載後獲取了一次數據。可是如何使用輸入字段告訴API咱們感興趣的主題?「Redux」作爲默認查詢。可是若是想要查詢關於「React」的呢?讓咱們實現一個input元素,能夠得到「Redux」以外的話題。所以,就要爲input元素引入一個新的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(() => {
...
}, []);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<ul>
...
</ul>
</Fragment>
);
}
export default App;
複製代碼
如今,請求數據和查詢參數這兩個 state 相互獨立,可是咱們但願將它們耦合起來,以獲取輸入框輸入的參數指定的話題文章。經過如下修改,組件應該在掛載後按照查詢參數獲取相應文章。
...
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中輸入一些內容時,在掛載後就不會再獲取任何數據了,由於咱們提供了[]
做爲第二個參數,effect沒有依賴任何變量,所以只會在掛載階段觸發,可是如今的effect應該依賴query
,每當query
改變的時候,就應該從新獲取數據。
...
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
...
}, [query]);
return (
...
);
}
export default App;
複製代碼
好了,如今input的值改變就會從新獲取數據。可是又出現另一個問題:每次輸入一個新字符,就會觸發 effect 進行一次新的請求。那麼咱們如何提供一個按鈕來手動觸發數據請求呢?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('');
useEffect(() => {
...
}, [query]);
return (
<Fragment>
<input
type="text"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<button type="button" onClick={() => setSearch(query)}>
Search
</button>
<ul>
...
</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相同,由於組件在掛載階段會請求一次數據,此時的結果也應該反映的是輸入框中的搜索條件。然而, search和 query有點相似,這看起來比較困惑。爲何不將請求的實際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。一旦在點擊或其餘effect中設置此state,此effect就會再次運行。在這種狀況下,若是 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;
複製代碼
一旦調用了effect獲取了數據,(在組件掛載階段或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;
複製代碼
每次hook從新運行時,error state都會被重置。這會頗有用,由於每次請求失敗後,用戶可能從新嘗試,這樣就可以重置錯誤。爲了觀察代碼是否生效,你能夠填寫一個無用的 URL ,而後檢查錯誤信息是否會出現。
如今咱們只有輸入框和按鈕進行組合,一旦引入更多的 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 Class組件中的實現方式相同。
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,提取出全部與數據請求相關的東西,除了輸入框的 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 };
}
複製代碼
如今,您的hook能夠再次在App組件中使用:
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>
);
}
複製代碼
初始狀態也能夠是通用的。將它簡單地傳遞給新的自定義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 和error state ,而且執行請求並將數據經過 hook 返回給組件。
目前爲止,咱們已經使用 state hooks 來管理了咱們獲取到的數據、Loading 狀態、error state。然而,全部的狀態都有屬於本身的 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函數和初始state做爲參數。在咱們的例子中,數據,Loading和error state的初始狀態的參數沒有改變,但它們已經聚合到一個由一個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 函數須要轉換的 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函數的實現。它須要處理三個不一樣的狀態轉換,稱爲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函數能夠經過其參數訪問當前狀態和傳入操做。到目前爲止,在out case 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();
}
};
複製代碼
如今,每一個狀態轉換(action.type決定)都返回一個基於以前 state 和 payload 的新狀態。例如,在請求成功的狀況下,payload 用於設置新 state 對象的 data 屬性。
總之,reducer hook 確保使用本身的邏輯封裝狀態管理的這一部分。經過提供 action type 和可選 payload ,老是會獲得可預測的狀態更改。此外,永遠不會遇到無效狀態。例如,之前可能會意外地將 isLoading
和 isError
設置爲true。在這種狀況下,UI中應該顯示什麼? 如今,由 reducer 函數定義的每一個 state 轉換都指向一個有效的 state 對象。
在React中的有一個常見問題,即便組件已經卸載(例如使用React Router切換了路由),仍會設置組件的state。我以前已經在這裏寫過關於這個問題的文章,它描述瞭如何防止在已經Unmount的組件中調用setState。讓咱們看看咱們如何阻止在數據提取的自定義鉤子中設置狀態:
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 Cancellation來實現),可是再也不爲卸載的組件執行狀態轉換。因爲 Axios 取消在我看來並非最好的API,因此這個防止設置狀態的布爾標誌也能夠完成這項工做。