使用React Hooks和Typescript获取数据

发布于:2021-02-04 12:00:20

0

774

0

react javascript typescript React Hooks

在React中重用逻辑是很复杂的,像HOCs和Render Props这样的模式试图解决这个问题。随着最近添加的hooks,重用逻辑变得更加容易。在本文中,我将展示一种使用hooks useEffectuseState从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;


ServiceInitServiceLoading分别定义任何操作之前和加载时web服务的状态。ServiceLoaded具有属性payload来存储从web服务加载的数据(请注意,我在这里使用的是泛型,因此我可以将该接口与有效负载的任何数据类型一起使用)。ServiceError具有属性error来存储可能发生的任何错误。使用此联合类型,如果我们在status属性中设置字符串'loading',并尝试为payloaderror属性赋值,则Typescript将失败,因为我们没有定义一个接口来允许status类型'loading'与名为payloaderror的属性一起使用。如果不进行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中的回调来检索新星际飞船的数据。

注意,在回调中,我调用setResultstatus设置为'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,并将该对象作为useEffecthooks的第二个参数传递。因为onChange处理程序会在每次击键时更改starship对象,所以每次用户键入时都会调用我们的web服务。

  • 我们需要使用hooksuseState来创建布尔状态submit只知道何时可以清理表单。我们可以使用这个布尔值作为usePostStarshipService的第二个参数来解决前面的问题,但这会使我们的代码复杂化。

  • 布尔值状态submit为我们的组件添加了逻辑,这些逻辑必须复制到重用我们的自定义hooks的其他组件上usePostStarshipService

有一个更好的方法,这次没有useEffecthooks

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服务做任何操作。这次我们没有使用useEffecthooks,而是创建了一个函数,它将接收要发送到web服务的表单数据并返回一个承诺。最后,我们返回一个带有service对象的对象和负责调用web服务的函数。

注意:我可以返回一个数组而不是自定义hooks中的一个对象,使其行为类似于useStatehooks,这样就可以任意命名组件中的名称。我决定返回一个对象,因为我认为没有必要重命名它们。如果愿意,您可以自由地返回数组。

我们的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;

我正在使用useStatehooks来管理窗体的状态,但是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代码(和其他编辑器可能)。不管怎样,如果你喜欢的话,你可以用布尔。