- 原文地址:How to fetch data with React Hooks?
- 原文做者:Robin Wieruch
- 原文源代碼地址:use data api hook
- 本文地址:如何用`React Hooks`獲取數據
- 本文源代碼地址:fetch data with react hooks
Stackblitz
地址:fetch data with react hooks
- 注意:筆者將會使用
TypeScript
來完成文章中的代碼,並使用ant design
來美化界面樣式- 若是文章對你有用的話,歡迎`star`
在這個教程中,我想向您展現如何經過state
和effect
鉤子來獲取數據。咱們將會使用廣爲人知的Hacker News API從科技世界獲取流行文章。經過這篇文章,你也能爲數據獲取實現本身的自定義hook
,它能夠在你的應用中的任何地方被複用或者發佈到npm
做爲一個獨立的node
包。javascript
若是你徹底不瞭解這個React
新特性,查閱這裏:introduction to React Hooks。若是你想要查閱如何在React
中使用hooks
來獲取數據示例的完成項目,能夠查閱這個GitHub
倉庫。html
若是你只想準備去使用React Hook
來獲取數據:執行npm install use-data-api
而且跟隨文檔來使用。若是你用到了它,別忘記爲它點一個star
。java
注意:在將來,React
並不打算用React Hooks
來獲取數據。相反的,一個叫作Suspense
的特性將會負責這件事。不過,下列的講解是一個很好的用來學習更多關於React
中state
和effect
hooks
內容的方式。node
若是你不熟悉React
中的數據獲取,能夠查閱個人文章React
中如何數據獲取。它將帶你瞭解如何使用React
類組件來進行數據獲取,如何使用Render Prop Components
和高階組件(Higher-Order Component s
)來使代碼變的可複用,如何進行錯誤處理和加載loading
狀態。在這篇文章,我想用函數組件中的React Hooks
來向你展現這些內容。react
import React, { useEffect, useState } from 'react';
const App: React.FC = () => {
const [data, setData] = useState<{ hits: any[] }>({ 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)。state
和更新state
的函數來自於一個叫作useState
的hook
,它負責管理咱們要爲App
組件取回數據的局部狀態。初始狀態用一個對象中空的hits
數組來表示列表數據,尚未人爲該數據設置任何狀態。ios
咱們將要使用axios
來獲取數據,可是想要使用其它的數據獲取庫或者瀏覽器原生的fetch API
將取決於你。若是你尚未安裝axios
, 你能夠經過命令行使用npm install axios
來作這件事,而後實現獲取數據的effect hook
。git
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const App: React.FC = () => {
const [data, setData] = useState<{ hits: any[] }>({ hits: [] });
useEffect(async() => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
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;
複製代碼
effect hook
叫作useEffect
,它能夠用axios
從API
獲取數據而且使用組件state hook
的更新函數來設置該組件局部狀態的數據。這裏咱們用async/await
來解決異步的Promise
。github
然而,當你運行你的應用的時候,你應該會陷入一個討厭的循環。effect hook
不只會在組件掛載後運行,也會在組件更新後運行。因爲咱們在獲取數據後使用setData
設置了state
,這會更新組件而且使effect
再次運行。而當effect
再次運行的時候,它又會再次獲取數據並調用setData
,周而復始。這是一個須要避免的bug
,咱們只想在組件渲染後獲取數據。這就是爲何要爲effect hook
傳一個空數組([]
)做爲第二個參數的緣由:避免effect hook
在組件更新後激活而僅僅是在組件掛載後激活。web
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const App: React.FC = () => {
const [data, setData] = useState<{ hits: any[] }>({ hits: [] });
useEffect(async() => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
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
在組件更新後將再也不運行,由於它沒有必要監測任何變量。npm
還有最後一個陷阱。在代碼中,咱們使用async/await
從第三方API
獲取數據。按照文檔所說,每一個用async
標註的函數都會返回一個隱式的promise
: "async
函數聲明定義一個返回AsyncFunction
對象的異步函數。異步函數是經過事件循環異步執行而且使用一個隱式的promise
返回結果的函數"。然而,一個effect hook
應該什麼都不返回或者返回一個清理函數。這就是爲何你可能會在你的開發者控制檯日誌看到以下警告:07:41:22.910 index.js 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
函數必須返回一個清理函數或者什麼都不返回。Promises
和useEffect(async() => ...))
是不支持的,可是你能夠在effect
內部調用一個async
函數).這也是爲何在useEffect
函數裏直接使用async
關鍵字不被容許的緣由。讓咱們爲這種狀況想一種變通方案:在effect
內部使用async
函數。(譯註者:下邊的代碼使用了ant design
,以後再也不提示)
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
useEffect(() => {
const fetchData = async () => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=react');
setData({ hits: result.data.hits });
};
fetchData().then();
}, []);
return (
<Card bordered={false}>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
譯註者:hacker news
的響應數據TypeScript
類型以下:
// responseTypes.ts
export interface IData {hits: IHit[];}
interface IHit {
title: string;
url: string;
author: string;
points: number;
story_text?: any;
comment_text?: any;
_tags: string[];
num_comments: number;
objectID: string;
_highlightResult: IHighlightResult;
}
interface IHighlightResult {
title: ITitle;
url: ITitle;
author: IAuthor;
}
interface IAuthor {
value: string;
matchLevel: string;
matchedWords: string[];
}
interface ITitle {
value: string;
matchLevel: string;
matchedWords: any[];
}
複製代碼
簡單來講,這就是使用React hooks
來獲取數據。可是若是你對錯誤處理、展現loading
狀態、如何觸發從表單獲取數據、以及如何實現一個可複用的數據獲取hook
感興趣的話,請繼續閱讀。
很好,一旦組件掛載咱們就會去獲取數據。可是使用一個輸入字段來告訴API
咱們對哪一個話題感興趣呢?"Redux"被做爲默認查詢,可是關於"React"的話題呢?接下來,讓咱們實現一個input
輸入框,可以讓用戶獲取除"Redux"外的其它相關文章。所以,咱們爲輸入元素引入一個新的狀態。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios('https://hn.algolia.com/api/v1/search?query=redux');
setData({ hits: result.data.hits });
};
fetchData().then();
}, []);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
此時,倆個狀態彼此之間互相獨立,可是如今你想要組合它們只獲取輸入框中指定查詢條件的文章數據。一旦組件掛載後,應該經過查詢項來獲取全部文章。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
setData({ hits: result.data.hits });
};
fetchData().then();
}, []);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
這裏有一起被忽略了:當你嘗試在輸入框中輸入一些文字後,組件從新渲染後並無進行數據獲取。那是由於你提供了空數組做爲effect
的第二個參數,effect
不依賴於任何變量,所以它只會在組件掛載後觸發。然而,effect
如今應該依賴於query
,一旦query
改變,數據請求將會再次觸發。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
const [query, setQuery] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(`https://hn.algolia.com/api/v1/search?query=${query}`);
setData({ hits: result.data.hits });
};
fetchData().then();
}, [query]);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
一旦你改變了輸入框中的值數據將會從新獲取。可是這也引起了另一個問題:在你每爲輸入框輸入一個字符的時候,effect
將會被觸發而且執行另外一次數據獲取請求。那麼如何提供一個按鈕來觸發請求從而手動的地執行hook
呢?
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
const [query, setQuery] = useState('redux');
const [search, setSearch] = useState('redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(`https://hn.algolia.com/api/v1/search?query=${search}`);
setData({ hits: result.data.hits });
};
fetchData().then();
}, [search]);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button onClick={() => setSearch(query)}>Search</Button>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
如今,咱們使effect
依賴於search
狀態而不是隨着在輸入框中每一次擊鍵而發生變化的query
狀態。一旦用戶點擊按鈕,新的search
狀態被設置而且手動地觸發effect hook
。
search
狀態的初始值也被設置爲和query
狀態同樣。由於組件也在掛載後獲取數據,而且結果應該和輸入框中的值做爲查詢條件獲取到的數據相同。然而,具備相似的query
和search
狀態有一點讓人疑惑。爲何不設置當前的請求地址做爲狀態來替代search
狀態?
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState('https://hn.algolia.com/api/v1/search?query=redux');
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setData({ hits: result.data.hits });
};
fetchData().then();
}, [url]);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
<List dataSource={data.hits} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
上面的例子就是用effect hook
編程地/手動地獲取數據。你能夠決定effect
依賴於哪個狀態。一旦你在點擊事件或在其它的反作用(useEffect
)中設置狀態(setState
),對應的effect
將會再次運行。在上邊的例子中,若是url
狀態發生改變,effect
將會再次運行並從API
中獲取文章數據。
讓咱們繼續介紹數據獲取時的loading
展現。本質上,loading
只是被state hook
所管理的另一個state
。在App
組件中,loading
標識用來渲染一個loading
指示器。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ 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 fetchData = async () => {
setIsLoading(true);
const result = await axios(url);
setData({ hits: result.data.hits });
setIsLoading(false);
};
fetchData().then();
}, [url]);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>
</Card>
);
};
export default App;
複製代碼
當組件掛載或者url
狀態發生改變,effect
被調用來獲取數據,loading
狀態將被設置爲true
。一旦請求成功獲取到數據,loading
狀態將會再次被設置爲false
。
怎樣用React hook
來爲數據獲取進行錯誤處理呢?error
只是用state hook
初始化的另一個state
,一旦有一個錯誤狀態,App
組件會爲用戶渲染錯誤頁面。當使用async/await
的時候,使用try/catch
來進行錯誤處理是常見的,你能夠在effect
裏來作這件事。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const FetchData: React.FC = () => {
const [data, setData] = useState<IData>({ 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 fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData({ hits: result.data.hits });
} catch (e) {
setIsError(true);
}
setIsLoading(false);
};
fetchData().then();
}, [url]);
return (
<Card bordered={false}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button onClick={() => setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`)}>Search</Button>
{isError ?
<div>something went wrong...</div>
:
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>}
</Card>
);
};
export default FetchData;
複製代碼
每次hooks
再次運行的時候錯誤狀態將會被重置。這是有用的,由於在請求失敗後用戶可能想要再次嘗試,這個時候應該重置錯誤。爲了強行製造一個錯誤你能夠將url
改成某些無效的值,而後檢查錯誤信息是否展現。
怎樣用一個合適的表單來獲取數據呢?目前爲止,咱們只有輸入框和按鈕進行組合。一旦你想要引入更多的input
元素,你可能想要用一個form
元素來包裹它們。此外,form
也可讓你用回車鍵來觸發search
按鈕。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const App: React.FC = () => {
const [data, setData] = useState<IData>({ 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 fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData({ hits: result.data.hits });
} catch (e) {
setIsError(true);
}
setIsLoading(false);
};
fetchData().then();
}, [url]);
return (
<Card bordered={false}>
<form onSubmit={(e) => {
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
}}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button htmlType="submit">Search</Button>
</form>
{isError ?
<div>something went wrong...</div>
:
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>}
</Card>
);
};
export default App;
複製代碼
可是如今當點擊提交按鈕的時候,瀏覽器將會從新加載,這是在提交一個表單時瀏覽器的默認行爲。爲了阻止默認行爲,咱們要在React
事件內調用一個函數。在React
類組件中你也能夠這樣作。
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Button, Card, Input, List } from 'antd';
import { IData } from '@/responseTypes';
const FetchData: React.FC = () => {
const [data, setData] = useState<IData>({ 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 fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData({ hits: result.data.hits });
} catch (e) {
setIsError(true);
}
setIsLoading(false);
};
fetchData().then();
}, [url]);
return (
<Card bordered={false}>
<form onSubmit={(e) => {
e.preventDefault();
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
}}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button htmlType="submit">Search</Button>
</form>
{isError ?
<div>something went wrong...</div>
:
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>}
</Card>
);
};
export default FetchData;
複製代碼
如今,當你點擊提交按鈕後瀏覽器再也不會從新加載。它和以前同樣工做,但這時是一個表單而不是原生輸入框和按鈕的組合,你也能夠按下你鍵盤上的回車鍵來提交表單內容。
爲了爲獲取數據提取自定義hook
,除了屬於輸入框的query
狀態,移動包括loading
展現和錯誤處理在內全部屬於數據獲取的代碼到它本身的函數中。固然,也要確保從函數中返回全部在App
組件中所用到的必須的變量。
初始狀態也能變得通用,只須要簡單地將它傳給新的自定義hook
。
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import axios from 'axios';
interface IResult<T> {
data: T;
isLoading: boolean;
isError: boolean;
}
const useHackerNewsApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
const [data, setData] = useState<T>(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 (e) {
setIsError(true);
}
setIsLoading(false);
};
fetchData().then();
}, [url]);
return [{ data, isLoading, isError }, setUrl];
};
export default useHackerNewsApi;
複製代碼
Tip: 在
tsx
中箭頭函數定義泛型的語法
如今,你的新hook
能夠在App
組件中再次使用:
import React, { useState } from 'react';
import { Button, Card, Input, List } from 'antd';
import useHackerNewsApi from '@/views/fetchData/useHackerNewsApi';
import { IData } from '@/responseTypes';
const FetchData: React.FC = () => {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, setUrl] = useHackerNewsApi<IData>({ hits: [] }, 'https://hn.algolia.com/api/v1/search?query=redux');
return (
<Card bordered={false}>
<form onSubmit={(e) => {
e.preventDefault();
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
}}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button htmlType="submit">Search</Button>
</form>
{isError ?
<div>something went wrong...</div>
:
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>}
</Card>
);
};
export default FetchData;
複製代碼
這就是用一個自定義hook
來獲取數據。hook
自己並不知道關於API
的任何內容,它接收全部來自外部的參數,而且只管理必要的狀態,好比:data
,loading
,error
。它就像自定義請求數據hook
同樣爲使用到它的組件發起請求而且返回數據。
到目前爲止,咱們已經使用多個state hook
來管理咱們的數據獲取狀態data
,加載狀態loading
和錯誤狀態error
。然而,用單獨的state hook
管理的全部這些狀態都應該屬於同一類,由於它們關心相同的問題。正如你看到的,他們都在數據獲取函數中被用到。它們是一個接一個地使用的(好比:setIsError
,setIsLoading
),這能夠很好的代表它們是在一塊兒的。讓咱們將三個狀態所有與Reducer Hook
結合使用。
Reducer Hook
爲咱們返回一個state
對象以及一個更改state
對象的函數。這個函數叫作派發(dispatch
)函數,它接收一個擁有type
和可選的payload
的action
做爲參數。全部的這些信息用來從以前的狀態以及action
的可選的payload
和type
來提取一個新的state
。讓咱們看一下這在代碼中是如何工做的:
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
對象做爲參數。在咱們的例子中,data
,loading
和error
狀態的初始參數是不會變化的,可是它們被聚合到了一個狀態對象,經過一個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
函數哪個狀態轉換須要被應用,payload
被用來從reducer
提取新的state
。最終,咱們只有三種狀態轉換:初始化數據獲取過程、數據獲取結果成功的通知、數據獲取結果異常的通知。
在自定義hook
的最後,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,
});
...
return [state, setUrl];
};
複製代碼
最後很重要的一點,咱們少了對reducer
函數的實現。它須要處理三種不一樣的狀態轉換,分別是FETCH_INIT
,FETCH_SUCCESS
和FETCH_FAILURE
。每一種狀態轉換須要返回一個新的state
對象。讓咱們看看如何經過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
函數能夠經過參數來訪問當前的狀態state
和執行dispatch
時傳入的action
。目前爲止,在switch case
語句中每個狀態轉換隻返回了以前的state
。展開語法用來保證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
決定的每一次狀態轉換,將會基於以前的狀態和可選的payload
返回一個新的狀態。好比,在一次成功請求的狀況下,payload
被用來設置新的狀態對象的data
屬性。
總之,Reducer Hook
確保這部分狀態管理用它本身的邏輯來封裝。經過提供action types
和可選的payload
,你將總會用一個可預測的狀態改變來更新state
。此外,你將永遠不會遇到無效的狀態。例如,在這以前,isLoading
和isError
狀態可能會被意外的都設置爲true
。這種狀況下UI
應該顯示什麼呢?如今,經過reducer
函數定義的每一個狀態變換都會指向一個有效的state
對象。
譯者注:全部的自定義hook
的源碼以下
import { Dispatch, Reducer, SetStateAction, useEffect, useReducer, useState } from 'react';
import axios from 'axios';
interface IResult<T = any> {
data: T;
isLoading: boolean;
isError: boolean;
}
type IAction<T = any> = {
type: 'FETCH_INIT';
} | {
type: 'FETCH_SUCCESS';
payload: T
} | {
type: 'FETCH_FAILURE';
}
const dataFetchReducer = <T extends any> (state: IResult<T>, action: IAction<T>): IResult<T> => {
switch (action.type) {
case 'FETCH_INIT':
return { ...state, isLoading: true };
case 'FETCH_SUCCESS':
return {
...state,
data: action.payload,
isLoading: false,
isError: false
};
case 'FETCH_FAILURE':
return { ...state, isError: true };
default:
throw new Error();
}
};
const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {
data: initialData,
isError: false,
isLoading: false
});
const [url, setUrl] = useState(initialUrl);
useEffect(() => {
const fetchData = async () => {
dispatch({ type: 'FETCH_INIT' });
try {
const result = await axios(url);
dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
} catch (e) {
dispatch({ type: 'FETCH_FAILURE' });
}
};
fetchData().then();
}, [url]);
return [state, setUrl];
};
export { useDataApi };
複製代碼
在組件中這樣使用:
import React, { useState } from 'react';
import { Button, Card, Input, List } from 'antd';
import { useDataApi } from '@/views/fetchData/useHackerNewsApi';
import { IData } from '@/responseTypes';
const FetchData: React.FC = () => {
const [query, setQuery] = useState('redux');
const [{ data, isLoading, isError }, setUrl] = useDataApi<IData>({ hits: [] }, 'https://hn.algolia.com/api/v1/search?query=redux');
return (
<Card bordered={false}>
<form onSubmit={(e) => {
e.preventDefault();
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}`);
}}>
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<Button htmlType="submit">Search</Button>
</form>
{isError ?
<div>something went wrong...</div>
:
<List dataSource={data.hits} loading={isLoading} renderItem={(item) => (
<List.Item>
<a href={item.url}>{item.title}</a>
</List.Item>
)}/>}
</Card>
);
};
export default FetchData;
複製代碼
即便組件已經卸載(好比因爲使用React Router
導航離開當前組件)可是組件的狀態仍是會被設置,這在React
中是一個常見的問題。我以前在這裏寫過關於這個問題的文章,它描述了在各類場景下如何阻止卸載組件設置狀態。讓咱們看一下如何在獲取數據的自定義hook
中阻止設置狀態:
const useDataApi = <T extends any> (initialData: T, initialUrl: string): [IResult<T>, Dispatch<SetStateAction<string>>] => {
const [state, dispatch] = useReducer<Reducer<IResult<T>, IAction<T>>>(dataFetchReducer, {
data: initialData,
isError: false,
isLoading: false
});
const [url, setUrl] = useState(initialUrl);
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 (e) {
if (!didCancel) {
dispatch({ type: 'FETCH_FAILURE' });
}
}
};
fetchData().then();
return () => {
didCancel = true;
};
}, [url]);
return [state, setUrl];
};
複製代碼
在組件卸載的時候,每個Effect Hook
所對應的清理函數將會運行。清理函數是一個從hook
裏返回的函數。在咱們的例子中,咱們使用一個叫作didCancel
的布爾值做爲標識來讓數據獲取邏輯知道組件的狀態(掛載/卸載)。若是組件已經卸載了,應該將標記設置爲true
,從而阻止在異步解析數據以後設置組件的狀態。
注意:實際上數據獲取並無被終止(終止數據獲取能夠用Axios Cancellation
來實現),可是已經掛載的組件再也不執行狀態轉換。由於Axios Cancellation
在我眼中不是最好的API
,這個布爾值也能作阻止組件設置狀態的工做。