發(fā)布于:2021-02-04 12:00:20
0
815
0
在React中重用邏輯是很復(fù)雜的,像HOCs和Render Props這樣的模式試圖解決這個問題。隨著最近添加的hooks,重用邏輯變得更加容易。在本文中,我將展示一種使用hooks useEffect
和useState
從web服務(wù)加載數(shù)據(jù)的簡單方法(我正在使用斯瓦皮公司在裝載星戰(zhàn)星際飛船的例子中)以及如何輕松管理裝載狀態(tài)。作為獎勵,我用的是打字。我將建立一個簡單的應(yīng)用程序來買賣星球大戰(zhàn)星際飛船,你可以在這里看到最終的結(jié)果https://camilosw.github.io/react-hooks-services。
加載初始數(shù)據(jù)
在Reacthooks發(fā)布之前,從web服務(wù)加載初始數(shù)據(jù)的最簡單方法是componentDidMount
:
class Starships extends React.Component {
state = {
starships: [],
loading: true,
error: false
}
componentDidMount () {
fetch('https://swapi.co/api/starships')
.then(response => response.json())
.then(response => this.setState({
starships: response.results,
loading: false
}))
.catch(error => this.setState({
loading: false,
error: true
}));
}
render () {
const { starships, loading, error } = this.state;
return ({loading &&Loading...}
{!loading && !error &&
starships.map(starship => ({starship.name}))
}
{error &&Error message});
}
};
但是重用這些代碼是很困難的,因為您無法從React 16.8之前的組件中提取行為。流行的選擇是使用高階組件或渲染道具,但是這些方法有一些缺點,如React Hooks文檔中所述https://reactjs.org/docs/hooks intro.html-組件間難以重用的有狀態(tài)邏輯。
使用hooks,我們可以將行為提取到自定義hooks中,這樣就可以在任何組件中輕松地重用它。如果您不知道如何創(chuàng)建自定義掛鉤,請先閱讀文檔:https://reactjs.org/docs/hooks-custom.html。
因為我們使用的是Typescript,首先我們需要定義我們希望從web服務(wù)接收的數(shù)據(jù)的形狀,所以我定義了接口Starship
:
export interface Starship {
name: string;
crew: string;
passengers: string;
cost_in_credits?: string;
url: string;
}
因為我們將處理具有多個狀態(tài)的web服務(wù),所以我為每個狀態(tài)定義了一個接口。最后,我將Service
定義為這些接口的聯(lián)合類型:
interface ServiceInit {
status: 'init';
}
interface ServiceLoading {
status: 'loading';
}
interface ServiceLoaded{
status: 'loaded';
payload: T;
}
interface ServiceError {
status: 'error';
error: Error;
}
export type Service=
| ServiceInit
| ServiceLoading
| ServiceLoaded| ServiceError;
ServiceInit
和ServiceLoading
分別定義任何操作之前和加載時web服務(wù)的狀態(tài)。ServiceLoaded
具有屬性payload
來存儲從web服務(wù)加載的數(shù)據(jù)(請注意,我在這里使用的是泛型,因此我可以將該接口與有效負(fù)載的任何數(shù)據(jù)類型一起使用)。ServiceError
具有屬性error
來存儲可能發(fā)生的任何錯誤。使用此聯(lián)合類型,如果我們在status
屬性中設(shè)置字符串'loading'
,并嘗試為payload
或error
屬性賦值,則Typescript將失敗,因為我們沒有定義一個接口來允許status
類型'loading'
與名為payload
或error
的屬性一起使用。如果不進行Typescript或任何其他類型檢查,代碼只有在運行時出錯時才會失敗。
定義了類型Service
和接口Starship
之后,現(xiàn)在可以創(chuàng)建自定義hooksusePostStarshipService
:
import { useEffect, useState } from 'react';
import { Service } from '../types/Service';
import { Starship } from '../types/Starship';
export interface Starships {
results: Starship[];
}
const usePostStarshipService = () => {
const [result, setResult] = useState<Service>({
status: 'loading'
});
useEffect(() => {
fetch('https://swapi.co/api/starships')
.then(response => response.json())
.then(response => setResult({ status: 'loaded', payload: response }))
.catch(error => setResult({ status: 'error', error }));
}, []);
return result;
};
export default usePostStarshipService;
這是在前面的代碼中發(fā)生的:
因為SWAPI在數(shù)組中返回一個星際飛船數(shù)組,所以我定義了一個新的接口,它包含屬性results
,作為一個數(shù)組的Starship。
自定義hooksusePostStarshipService
只是一個函數(shù),從文檔中建議的單詞use
開始:https://reactjs.org/docs/hooks custom.html-一個定制hooks。
在該函數(shù)內(nèi)部,我正在使用HookuseState來管理Web服務(wù)狀態(tài)。注意,我需要定義將由result傳遞通用狀態(tài)的狀態(tài)管理的確切數(shù)據(jù)類型<Service<Starship>>。我正在用ServiceInit聯(lián)合類型的接口初始化Hook Service,所以唯一允許的屬性是status字符串'loading'。
我還使用useEffect帶有回調(diào)的Hook作為第一個參數(shù)來從Web服務(wù)中獲取數(shù)據(jù),并使用空數(shù)組作為第二個參數(shù)。第二個參數(shù)告訴useEffect您執(zhí)行回調(diào)的條件是什么,并且因為我們傳遞的是空數(shù)組,所以該回調(diào)將僅被調(diào)用一次(有關(guān)useEffect您是否不熟悉Hook的更多信息,請參見https://reactjs.org/docs /hooks-effect.html)。
最后,我要返回result。該對象包含狀態(tài)以及由于調(diào)用Web服務(wù)而導(dǎo)致的任何有效負(fù)載或錯誤。這就是我們在組件中向用戶顯示W(wǎng)eb服務(wù)狀態(tài)和檢索到的數(shù)據(jù)所需要的。
請注意,我在上一個示例中使用的fetch
方法非常簡單,但對于生產(chǎn)代碼來說還不夠。例如,catch只捕獲網(wǎng)絡(luò)錯誤,而不是4xx或5xx錯誤。在您自己的代碼中,最好創(chuàng)建另一個包裝fetch
的函數(shù)來處理錯誤、標(biāo)題等。
現(xiàn)在,我們可以使用我們的hooks來檢索星際飛船列表并將它們顯示給用戶:
們使用的是Typescript,首先我們需要定義我們希望從web服務(wù)接收的數(shù)據(jù)的形狀,所以我定義了接口Starship
:
import React from 'react';
import useStarshipsService from '../services/useStarshipsService';
const Starships: React.FC<{}> = () => {
const service = useStarshipsService();
return (
<div>
{service.status === 'loading' && <div>Loading...</div>}
{service.status === 'loaded' &&
service.payload.results.map(starship => (
<div key={starship.url}>{starship.name}</div>
))}
{service.status === 'error' && (
<div>Error, the backend moved to the dark side.</div>
)}
</div>
);
};
export default Starships;
這次,我們的自定義hooks將管理狀態(tài),因此我們只需要根據(jù)返回的service
對象的status
屬性有條件地呈現(xiàn)。
請注意,如果在狀態(tài)為'loading'
時嘗試訪問payload
,TypeScript將失敗,因為payload
只存在于ServiceLoaded
接口中,而不存在于ServiceLoading
接口中:
TypeScript非常聰明,知道如果status
屬性和字符串'loading'
之間的比較為真,則相應(yīng)的接口是ServiceLoaded
,在這種情況下,starships
對象沒有payload
屬性。
狀態(tài)更改時加載內(nèi)容
在我們的示例中,如果用戶單擊任何星艦,我們將更改組件上的狀態(tài)以設(shè)置所選的星艦,并使用與該星艦對應(yīng)的url調(diào)用web服務(wù)(注意https://swapi.co/api/starships加載每艘星際飛船的所有數(shù)據(jù),因此無需再次加載該數(shù)據(jù)。我這樣做只是為了演示。)
傳統(tǒng)上,我們使用componentdiddupdate來檢測狀態(tài)變化并執(zhí)行相應(yīng)的操作:
class Starship extends React.Component {
...
componentDidUpdate(prevProps) {
if (prevProps.starship.url !== this.props.starship.url) {
fetch(this.props.starship.url)
.then(response => response.json())
.then(response => this.setState({
starship: response,
loading: false
}))
.catch(error => this.setState({
loading: false,
error: true
}));
}
}
...};
如果我們需要在不同的道具和狀態(tài)屬性發(fā)生變化時做出不同的動作,componentDidUpdate
很快就會變得一團糟。使用hooks,我們可以將這些操作封裝在單獨的自定義hooks中。在本例中,我們將創(chuàng)建一個自定義hooks來提取componentDidUpdate
中的行為,就像我們之前所做的那樣:
import { useEffect, useState } from 'react';
import { Service } from '../types/Service';
import { Starship } from '../types/Starship';
const useStarshipByUrlService = (url: string) => {
const [result, setResult] = useState<Service<Starship>>({
status: 'loading'
});
useEffect(() => {
if (url) {
setResult({ status: 'loading' });
fetch(url)
.then(response => response.json())
.then(response => setResult({ status: 'loaded', payload: response }))
.catch(error => setResult({ status: 'error', error }));
}
}, [url]);
return result;
};
export default useStarshipByUrlService;
這一次,我們的自定義hooks接收url作為參數(shù),并將其用作hooks的第二個參數(shù)。這樣,每當(dāng)url改變時,就會調(diào)用useEffect
中的回調(diào)來檢索新星際飛船的數(shù)據(jù)。
注意,在回調(diào)中,我調(diào)用setResult
將status
設(shè)置為'loading'
。這是因為回調(diào)將被多次調(diào)用,所以我們需要在開始獲取之前重置狀態(tài)。
在我們的Starship
組件中,我們將url作為一個道具接收,并將其傳遞給我們的定制hooksuseStarshipByUrlService
。每當(dāng)父組件中的url發(fā)生更改時,我們的自定義hooks將再次調(diào)用web服務(wù)并為我們管理狀態(tài):
import React from 'react';
import useStarshipByUrlService from '../services/useStarshipByUrlService';
export interface Props {
url: string;
}
const Starship: React.FC<Props> = ({ url }) => {
const service = useStarshipByUrlService(url);
return (
<div>
{service.status === 'loading' && <div>Loading...</div>}
{service.status === 'loaded' && (
<div>
<h2>{service.payload.name}</h2>
...
</div>
)}
{service.status === 'error' && <div>Error message</div>}
</div>
);
};
export default Starship;
正在發(fā)送內(nèi)容
發(fā)送內(nèi)容類似于在狀態(tài)更改時加載內(nèi)容。在第一種情況下,我們向自定義hooks傳遞了一個url,現(xiàn)在我們可以傳遞一個包含要發(fā)送的數(shù)據(jù)的對象。如果我們嘗試這樣做,代碼將是這樣的:
const usePostStarshipService = (starship: Starship) => {
const [result, setResult] = useState<Service<Starship>>({
status: 'init'
});
useEffect(() => {
setResult({ status: 'loading' });
fetch('https://swapi.co/api/starships', {
method: 'POST',
body: JSON.stringify(starship)
})
.then(response => response.json())
.then(response => {
setResult({ status: 'loaded', payload: response });
})
.catch(error => {
setResult({ status: 'error', error });
});
}, [starship]);
return result;
};
const CreateStarship: React.FC<{}> = () => {
const initialStarshipState: Starship = {
name: '',
crew: '',
passengers: '',
cost_in_credits: ''
};
const [starship, setStarship] = useState<PostStarship>(initialStarshipState);
const [submit, setSubmit] = useState(false);
const service = usePostStarshipService(starship);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.persist();
setStarship(prevStarship => ({
...prevStarship,
[event.target.name]: event.target.value
}));
};
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmit(true);
};
useEffect(() => {
if (submit && service.status === 'loaded') {
setSubmit(false);
setStarship(initialStarshipState);
}
}, [submit]);
return (
<form onSubmit={handleFormSubmit}>
<input
type="text"
name="name"
value={starship.name}
onChange={handleChange}
/>
...
</form>
)
}
但之前的代碼有一些問題:
我們將starship
對象傳遞給自定義hooks,并將該對象作為useEffect
hooks的第二個參數(shù)傳遞。因為onChange處理程序會在每次擊鍵時更改starship
對象,所以每次用戶鍵入時都會調(diào)用我們的web服務(wù)。
我們需要使用hooksuseState
來創(chuàng)建布爾狀態(tài)submit
只知道何時可以清理表單。我們可以使用這個布爾值作為usePostStarshipService
的第二個參數(shù)來解決前面的問題,但這會使我們的代碼復(fù)雜化。
布爾值狀態(tài)submit
為我們的組件添加了邏輯,這些邏輯必須復(fù)制到重用我們的自定義hooks的其他組件上usePostStarshipService
有一個更好的方法,這次沒有useEffect
hooks:
import { useState } from 'react';
import { Service } from '../types/Service';
import { Starship } from '../types/Starship';
export type PostStarship = Pick<
Starship,
'name' | 'crew' | 'passengers' | 'cost_in_credits'
>;
const usePostStarshipService = () => {
const [service, setService] = useState<Service<PostStarship>>({
status: 'init'
});
const publishStarship = (starship: PostStarship) => {
setService({ status: 'loading' });
const headers = new Headers();
headers.append('Content-Type', 'application/json; charset=utf-8');
return new Promise((resolve, reject) => {
fetch('https://swapi.co/api/starships', {
method: 'POST',
body: JSON.stringify(starship),
headers
})
.then(response => response.json())
.then(response => {
setService({ status: 'loaded', payload: response });
resolve(response);
})
.catch(error => {
setService({ status: 'error', error });
reject(error);
});
});
};
return {
service,
publishStarship
};
};
export default usePostStarshipService;
首先,我們創(chuàng)建了一個新的PostStarship
類型,它派生自Starship
,選擇將發(fā)送到web服務(wù)的屬性。在我們的自定義hooks中,我們使用屬性status
中的字符串'init'
初始化服務(wù),因為調(diào)用時usePostStarshipService
不會對web服務(wù)做任何操作。這次我們沒有使用useEffect
hooks,而是創(chuàng)建了一個函數(shù),它將接收要發(fā)送到web服務(wù)的表單數(shù)據(jù)并返回一個承諾。最后,我們返回一個帶有service
對象的對象和負(fù)責(zé)調(diào)用web服務(wù)的函數(shù)。
注意:我可以返回一個數(shù)組而不是自定義hooks中的一個對象,使其行為類似于useState
hooks,這樣就可以任意命名組件中的名稱。我決定返回一個對象,因為我認(rèn)為沒有必要重命名它們。如果愿意,您可以自由地返回數(shù)組。
我們的CreateStarship
組件這次將更簡單:
import React, { useState } from 'react';
import usePostStarshipService, {
PostStarship
} from '../services/usePostStarshipService';
import Loader from './Loader';
const CreateStarship: React.FC<{}> = () => {
const initialStarshipState: PostStarship = {
name: '',
crew: '',
passengers: '',
cost_in_credits: ''
};
const [starship, setStarship] = useState<PostStarship>(
initialStarshipState
);
const { service, publishStarship } = usePostStarshipService();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
event.persist();
setStarship(prevStarship => ({
...prevStarship,
[event.target.name]: event.target.value
}));
};
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
publishStarship(starship).then(() => setStarship(initialStarshipState));
};
return (
<div>
<form onSubmit={handleFormSubmit}>
<input
type="text"
name="name"
value={starship.name}
onChange={handleChange}
/>
...
</form>
{service.status === 'loading' && <div>Sending...</div>}
{service.status === 'loaded' && <div>Starship submitted</div>}
{service.status === 'error' && <div>Error message</div>}
</div>
);
};
export default CreateStarship;
我正在使用useState
hooks來管理窗體的狀態(tài),但是handleChange
的行為與使用this.state
類內(nèi)組件時的行為相同。我們的usePostStarshipService
除了返回處于初始狀態(tài)的service
對象并返回publishStarship方法來調(diào)用web服務(wù)之外,什么都不做。提交表單并調(diào)用handleFormSubmit
時,我們使用表單數(shù)據(jù)調(diào)用publishStarship
?,F(xiàn)在,我們的service
對象開始管理web服務(wù)更改的狀態(tài)。如果返回的承諾成功,我們用initialStarshipState
調(diào)用setStarship
來清理表單。
僅此而已,我們有三個自定義hooks來檢索初始數(shù)據(jù)、檢索單個項和發(fā)布數(shù)據(jù)。您可以在這里看到完整的項目:https://github.com/camilosw/react-hooks-services
最后的想法
Reacthooks是一個很好的補充,但是當(dāng)有更簡單和完善的解決方案時,不要試圖過度使用它們,比如Promise,而不是我們發(fā)送內(nèi)容示例中的useEffect
。
使用hooks還有另一個好處。如果你仔細看,你會發(fā)現(xiàn)我們的組件基本上是呈現(xiàn)的,因為我們把有狀態(tài)邏輯移到了定制的hooks上。有一個已建立的模式將邏輯與表示分離,稱為容器/表示,您將邏輯放在父組件中,將表示放在子組件中。這種模式最初是由丹·阿布拉莫夫構(gòu)思的,但現(xiàn)在我們有了hooks,丹·阿布拉莫夫建議少用這種模式,而使用hooks:https://medium.com/@dan_abramov/smart-和-dumb-components-7ca2f9a7c7d0。
也許你討厭使用字符串來命名狀態(tài),并責(zé)怪我這么做,但如果你使用Typescript,你是安全的,因為Typescript將失敗,如果你拼寫錯誤的狀態(tài)名稱,你將得到自動完成免費在VS代碼(和其他編輯器可能)。不管怎樣,如果你喜歡的話,你可以用布爾。