获取数据并创建自定义钩子
本教程将介绍如何获取数据并创建自定义钩子。
教程文章链接➡️
本文旨在介绍如何使用 React 和自定义 hook 来发出 HTTP GET 类型的请求。
🚨 注意:本文要求您了解 React 的基础知识(基本 hooks 和 fetch 请求)。
任何形式的反馈都非常欢迎,谢谢!希望您喜欢这篇文章。🤗
📌创建项目
📌第一步
📍 Card.tsx
▶️ React JS(版本 18)
▶️ Vite JS
▶️ TypeScript
▶️原生CSS(样式表可在本文末尾的仓库中找到)
npm init vite@latest
在这种情况下,我们将它命名为:(fetching-data-custom-hook可选)。
我们将选择 React,然后选择 TypeScript。
然后我们执行以下命令来导航到刚刚创建的目录。
cd fetching-data-custom-hook
然后我们安装依赖项:
npm install
然后我们在代码编辑器(我这里用的是 VS Code)中打开项目。
code .
在src/App.tsx文件夹中,我们删除该文件的所有内容,并放置一个显示标题和副标题的功能组件。
const App = () => {
return (
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
)
}
export default App;
首先,我们将创建几个接口,以帮助我们自动完成 API 提供的 JSON 响应中的属性。
Response包含 results 属性,该属性是一个 Results 数组。Result文档),选择角色的 ID、名称和图像。interface Response {
results: Result[]
}
interface Result {
id: number;
name: string;
image: string;
}
Result[],默认值为空数组,因为我们尚未发起 API 调用。这将用于存储 API 数据并使其能够显示。const App = () => {
const [data, setData] = useState<Result[]>([]);
return (
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
)
}
export default App;
useEffect,因为我们需要在我们的组件第一次渲染时执行获取操作。由于我们只需要它执行一次,所以我们放置一个空数组(即没有任何依赖项)。
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
},[]) // arreglo vació
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
</div>
)
}
export default App;
useEffect,将会发出 API 调用,由于useEffect不允许我们直接使用异步代码,因此我们将同时通过 Promise 发出调用。const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {})
.catch(console.log)
},[])
setData函数将其放入状态中。这样我们就可以把数据显示在屏幕上了。😌
🚨 如果 API 出现问题,catch 会捕获错误并通过控制台显示,并且状态“ ”的值data将保持为空数组(最后只会显示应用程序的标题和副标题)。
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
在显示 API 数据之前,我们需要进行评估。🤔
🔵 只有当“ ”状态值的长度data大于 0 时,我们才会在屏幕上显示 API 数据。
🔵 如果“ ”状态值的长度data小于或等于 0,则屏幕上不会显示任何数据,只会显示标题和副标题。
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && <p>data</p>
}
</div>
)
}
export default App;
现在,一旦我们确认“ ”状态值中确实有数据data,我们将继续显示和处理数据。
使用数组中的 map 函数,我们将遍历状态“ ”的值数组data,并返回一个新的 JSX 组件,在本例中,该组件仅包含图像和文本。
🔴 注意: div 元素内的key属性是 React 在列表中使用的标识符,用于更高效地渲染组件。务必设置此属性。
const App = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[])
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
这样我们就完成了数据获取和屏幕显示。不过我们还可以做得更好。😎
在src/hook文件夹中,我们创建一个名为 . 的文件useFetch。
我们创建了函数并删除了App.tsx组件的逻辑。
const App = () => {
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
我们将逻辑粘贴到此函数中,最后返回状态“ data”的值。
export const useFetch = () => {
const [data, setData] = useState<Result[]>([]);
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[]);
return {
data
}
}
最后,我们调用useFetch钩子函数来提取数据。
好了,我们的组件现在更简洁、更易读了。🤓
const App = () => {
const { data } = useFetch();
return (
<div>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
{
(data.length > 0) && data.map( ({ id, image, name }) => (
<div key={id}>
<img src={image} alt={image} />
<p>{name}</p>
</div>
))
}
</div>
)
}
export default App;
等等,我们还可以改进这个钩子。🤯
useFetch钩子。现在我们要做的就是改进钩子,添加更多属性。
在现有状态的基础上,我们将添加其他属性,并且这个新状态的类型为DataState。
interface DataState {
loading: boolean;
data: Result[];
error: string | null;
}
加载状态(布尔值)用于告知我们何时发起 API 调用。默认情况下,该值为 true。
如果返回错误信息(字符串或空值),则会显示错误信息。默认情况下,该值为空。
`data` 参数(类型为 `value` Result[])将显示 API 数据。默认情况下,该值为空数组。
🔴 注意:该庄园的房产已重新命名。
🔵数据➡️数据状态
🔵 setData ➡️ setDataState
export const useFetch = () => {
const [dataState, setDataState] = useState<DataState>({
data: [],
loading: true,
error: null
});
useEffect(()=> {
fetch('<https://rickandmortyapi.com/api/character/?page=8>')
.then( res => res.json())
.then( (res: Response) => {
setData(res.results);
})
.catch(console.log)
},[]);
return {
data
}
}
现在我们将逻辑提取useEffect到一个单独的函数中。这个函数将被命名为handleFetch.
我们将使用useCallback, 来存储此函数,并防止在状态改变时重新创建它。
它还useCallback接收一个依赖项数组,在这种情况下,我们将它留空,因为我们只想生成一次。
const handleFetch = useCallback(
() => {},
[],
)
接收参数的函数useCallback可以是异步的,因此我们可以使用async/await。
const handleFetch = useCallback(
async () => {
try {
const url = '<https://rickandmortyapi.com/api/character/?page=18>';
const response = await fetch(url);
} catch (error) {}
},
[],
)
const handleFetch = useCallback(
async () => {
try {
const url = '<https://rickandmortyapi.com/api/character/?page=18>';
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
const handleFetch = useCallback(
async () => {
try {
const url = '<https://rickandmortyapi.com/api/character/?page=18>';
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
const dataApi: Response = await response.json();
setDataState( prev => ({
...prev,
loading: false,
data: dataApi.results
}));
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
创建函数后handleFetch,我们返回到函数中useEffect,删除其中的逻辑并添加以下内容。
我们通过访问数据属性来判断状态“ dataState ”的值是否包含长度为0的数据,如果包含,则执行该函数。这样做是为了避免多次调用该函数。
useEffect(() => {
if (dataState.data.length === 0) handleFetch();
}, []);
钩子看起来是这样的:
🔴 注意:在钩子函数的末尾,我们使用扩展运算符返回“ dataState ”状态的值。
🔴 注意:接口已移至src/interfaces目录下的各自文件夹中。
import { useState, useEffect, useCallback } from 'react';
import { DataState, Response } from '../interface';
const url = '<https://rickandmortyapi.com/api/character/?page=18>';
export const useFetch = () => {
const [dataState, setDataState] = useState<DataState>({
data: [],
loading: true,
error: null
});
const handleFetch = useCallback(
async () => {
try {
const response = await fetch(url);
if(!response.ok) throw new Error(response.statusText);
const dataApi: Response = await response.json();
setDataState( prev => ({
...prev,
loading: false,
data: dataApi.results
}));
} catch (error) {
setDataState( prev => ({
...prev,
loading: false,
error: (error as Error).message
}));
}
},
[],
)
useEffect(() => {
if (dataState.data.length === 0) handleFetch();
}, []);
return {
...dataState
}
}
在使用此钩子的新属性之前,我们将进行重构并创建更多组件。😳
首先,需要在src 目录下创建components文件夹。
在 components 文件夹内,我们创建以下文件。
这个组件内部只会包含之前创建的标题和副标题。😉
export const Header = () => {
return (
<>
<h1 className="title">Fetching data and create custom Hook</h1>
<span className="subtitle">using Rick and Morty API</span>
</>
)
}
只有当钩子的 loading 属性设置为 true 时,此组件才会显示。⏳
export const Loading = () => {
return (
<p className='loading'>Loading...</p>
)
}
只有当钩子函数的错误属性包含字符串值时,才会显示此组件。🚨
export const ErrorMessage = ({msg}:{msg:string}) => {
return (
<div className="error-msg">{msg.toUpperCase()}</div>
)
}
显示 API 数据,即图像及其文本。🖼️
import { Result } from '../interface';
export const Card = ({ image, name }:Result) => {
return (
<div className='card'>
<img src={image} alt={image} width={100} />
<p>{name}</p>
</div>
)
}
此组件用作容器,用于遍历数据属性并显示字母及其信息。🔳
🔴 注意:我们使用 memo 来包裹组件,以避免重新渲染。虽然在这个应用中可能不会注意到这一点,但这只是一个小技巧。只有当“data”属性的值发生变化时,memo 函数才会重新渲染。
import { memo } from "react"
import { Result } from "../interface"
import { Card } from "./"
interface Props { data: Result[] }
export const LayoutCards = memo(({data}:Props) => {
return (
<div className="container-cards">
{
(data.length > 0) && data.map( character => (
<Card {...character} key={character.id}/>
))
}
</div>
)
})
这就是我们App.tsx组件的样子。
我们创建一个名为showData 的函数,并对其进行评估:
<Loading/>组件。<ErrorMessage/>,并将错误发送给组件。<LayoutCards/>并将数据发送给它以进行显示。最后,在组件下方,我们打开括号并调用showData函数。
import { ErrorMessage, Header, Loading, LayoutCards } from './components'
import { useFetch } from './hook';
const App = () => {
const { data, loading, error } = useFetch();
const showData = () => {
if (loading) return <Loading/>
if (error) return <ErrorMessage msg={error}/>
return <LayoutCards data={data} />
}
return (
<>
<Header/>
{ showData() }
</>
)
}
export default App;
🔴 注意:您也可以将showData函数移至 hook 中,并将 hook 文件扩展名更改为.tsx.jsx,这是因为在返回各种组件时会使用 JSX。
感谢你看到这里。🙌
如果你有兴趣,可以看看代码库。⬇️