发布于:2021-02-04 12:00:20
0
778
0
在React中重用逻辑是很复杂的,像HOCs和Render Props这样的模式试图解决这个问题。随着最近添加的hooks,重用逻辑变得更加容易。在本文中,我将展示一种使用hooks useEffect
和useState
从web服务加载数据的简单方法(我正在使用斯瓦皮公司在装载星战星际飞船的例子中)以及如何轻松管理装载状态。作为奖励,我用的是打字。我将建立一个简单的应用程序来买卖星球大战星际飞船,你可以在这里看到最终的结果https://camilosw.github.io/react-hooks-services。
加载初始数据
在Reacthooks发布之前,从web服务加载初始数据的最简单方法是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-组件间难以重用的有状态逻辑。
使用hooks,我们可以将行为提取到自定义hooks中,这样就可以在任何组件中轻松地重用它。如果您不知道如何创建自定义挂钩,请先阅读文档:https://reactjs.org/docs/hooks-custom.html。
因为我们使用的是Typescript,首先我们需要定义我们希望从web服务接收的数据的形状,所以我定义了接口Starship
:
export interface Starship {
name: string;
crew: string;
passengers: string;
cost_in_credits?: string;
url: string;
}
因为我们将处理具有多个状态的web服务,所以我为每个状态定义了一个接口。最后,我将Service
定义为这些接口的联合类型:
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服务的状态。ServiceLoaded
具有属性payload
来存储从web服务加载的数据(请注意,我在这里使用的是泛型,因此我可以将该接口与有效负载的任何数据类型一起使用)。ServiceError
具有属性error
来存储可能发生的任何错误。使用此联合类型,如果我们在status
属性中设置字符串'loading'
,并尝试为payload
或error
属性赋值,则Typescript将失败,因为我们没有定义一个接口来允许status
类型'loading'
与名为payload
或error
的属性一起使用。如果不进行Typescript或任何其他类型检查,代码只有在运行时出错时才会失败。
定义了类型Service
和接口Starship
之后,现在可以创建自定义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;
这是在前面的代码中发生的:
因为SWAPI在数组中返回一个星际飞船数组,所以我定义了一个新的接口,它包含属性results
,作为一个数组的Starship。
自定义hooksusePostStarshipService
只是一个函数,从文档中建议的单词use
开始:https://reactjs.org/docs/hooks custom.html-一个定制hooks。
在该函数内部,我正在使用HookuseState来管理Web服务状态。注意,我需要定义将由result传递通用状态的状态管理的确切数据类型<Service<Starship>>。我正在用ServiceInit联合类型的接口初始化Hook Service,所以唯一允许的属性是status字符串'loading'。
我还使用useEffect带有回调的Hook作为第一个参数来从Web服务中获取数据,并使用空数组作为第二个参数。第二个参数告诉useEffect您执行回调的条件是什么,并且因为我们传递的是空数组,所以该回调将仅被调用一次(有关useEffect您是否不熟悉Hook的更多信息,请参见https://reactjs.org/docs /hooks-effect.html)。
最后,我要返回result。该对象包含状态以及由于调用Web服务而导致的任何有效负载或错误。这就是我们在组件中向用户显示Web服务状态和检索到的数据所需要的。
请注意,我在上一个示例中使用的fetch
方法非常简单,但对于生产代码来说还不够。例如,catch只捕获网络错误,而不是4xx或5xx错误。在您自己的代码中,最好创建另一个包装fetch
的函数来处理错误、标题等。
现在,我们可以使用我们的hooks来检索星际飞船列表并将它们显示给用户:
们使用的是Typescript,首先我们需要定义我们希望从web服务接收的数据的形状,所以我定义了接口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将管理状态,因此我们只需要根据返回的service
对象的status
属性有条件地呈现。
请注意,如果在状态为'loading'
时尝试访问payload
,TypeScript将失败,因为payload
只存在于ServiceLoaded
接口中,而不存在于ServiceLoading
接口中:
TypeScript非常聪明,知道如果status
属性和字符串'loading'
之间的比较为真,则相应的接口是ServiceLoaded
,在这种情况下,starships
对象没有payload
属性。
状态更改时加载内容
在我们的示例中,如果用户单击任何星舰,我们将更改组件上的状态以设置所选的星舰,并使用与该星舰对应的url调用web服务(注意https://swapi.co/api/starships加载每艘星际飞船的所有数据,因此无需再次加载该数据。我这样做只是为了演示。)
传统上,我们使用componentdiddupdate来检测状态变化并执行相应的操作:
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
}));
}
}
...};
如果我们需要在不同的道具和状态属性发生变化时做出不同的动作,componentDidUpdate
很快就会变得一团糟。使用hooks,我们可以将这些操作封装在单独的自定义hooks中。在本例中,我们将创建一个自定义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作为参数,并将其用作hooks的第二个参数。这样,每当url改变时,就会调用useEffect
中的回调来检索新星际飞船的数据。
注意,在回调中,我调用setResult
将status
设置为'loading'
。这是因为回调将被多次调用,所以我们需要在开始获取之前重置状态。
在我们的Starship
组件中,我们将url作为一个道具接收,并将其传递给我们的定制hooksuseStarshipByUrlService
。每当父组件中的url发生更改时,我们的自定义hooks将再次调用web服务并为我们管理状态:
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;
正在发送内容
发送内容类似于在状态更改时加载内容。在第一种情况下,我们向自定义hooks传递了一个url,现在我们可以传递一个包含要发送的数据的对象。如果我们尝试这样做,代码将是这样的:
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的第二个参数传递。因为onChange处理程序会在每次击键时更改starship
对象,所以每次用户键入时都会调用我们的web服务。
我们需要使用hooksuseState
来创建布尔状态submit
只知道何时可以清理表单。我们可以使用这个布尔值作为usePostStarshipService
的第二个参数来解决前面的问题,但这会使我们的代码复杂化。
布尔值状态submit
为我们的组件添加了逻辑,这些逻辑必须复制到重用我们的自定义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;
首先,我们创建了一个新的PostStarship
类型,它派生自Starship
,选择将发送到web服务的属性。在我们的自定义hooks中,我们使用属性status
中的字符串'init'
初始化服务,因为调用时usePostStarshipService
不会对web服务做任何操作。这次我们没有使用useEffect
hooks,而是创建了一个函数,它将接收要发送到web服务的表单数据并返回一个承诺。最后,我们返回一个带有service
对象的对象和负责调用web服务的函数。
注意:我可以返回一个数组而不是自定义hooks中的一个对象,使其行为类似于useState
hooks,这样就可以任意命名组件中的名称。我决定返回一个对象,因为我认为没有必要重命名它们。如果愿意,您可以自由地返回数组。
我们的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来管理窗体的状态,但是handleChange
的行为与使用this.state
类内组件时的行为相同。我们的usePostStarshipService
除了返回处于初始状态的service
对象并返回publishStarship方法来调用web服务之外,什么都不做。提交表单并调用handleFormSubmit
时,我们使用表单数据调用publishStarship
。现在,我们的service
对象开始管理web服务更改的状态。如果返回的承诺成功,我们用initialStarshipState
调用setStarship
来清理表单。
仅此而已,我们有三个自定义hooks来检索初始数据、检索单个项和发布数据。您可以在这里看到完整的项目:https://github.com/camilosw/react-hooks-services
最后的想法
Reacthooks是一个很好的补充,但是当有更简单和完善的解决方案时,不要试图过度使用它们,比如Promise,而不是我们发送内容示例中的useEffect
。
使用hooks还有另一个好处。如果你仔细看,你会发现我们的组件基本上是呈现的,因为我们把有状态逻辑移到了定制的hooks上。有一个已建立的模式将逻辑与表示分离,称为容器/表示,您将逻辑放在父组件中,将表示放在子组件中。这种模式最初是由丹·阿布拉莫夫构思的,但现在我们有了hooks,丹·阿布拉莫夫建议少用这种模式,而使用hooks:https://medium.com/@dan_abramov/smart-和-dumb-components-7ca2f9a7c7d0。
也许你讨厌使用字符串来命名状态,并责怪我这么做,但如果你使用Typescript,你是安全的,因为Typescript将失败,如果你拼写错误的状态名称,你将得到自动完成免费在VS代码(和其他编辑器可能)。不管怎样,如果你喜欢的话,你可以用布尔。
作者介绍