通过示例讲解React的useEffect重构:学习公式文档
首先
2023年3月,React官方文档进行了更新。
尤其是关于useEffect的页面,像是”You Might Not Need an Effect”这一部分在Twitter等平台上引起了很大的讨论。
在本文中,将介绍MI-6公司的SaaS产品”miHub”开发团队根据上述文件进行重构工作中经常见到的一种模式,该模式可以消除useEffect。
选项1: 不必要的状态
如果props中传入的值是在父组件中进行fetch的数据,那么就会经常遇到这种情况的描述。
由于无法确定值何时被赋值,因此我试图使用useEffect确保计算结果被正确反映并进行渲染。
import React, { useState, useEffect } from 'react';
type = Props = {
count: number
max: number
}
export const Counter: React.FC<Props> = (props) => {
const [isOverMax, setIsOverMax] = useState<boolean>(false);
useEffect(() => {
setIsOverMax(props.count >= props.max);
}, [props.count, props.max]);
...
}
在这个例子中,以下的描述已经足够了。
export const Counter: React.FC<Props> = (props) => {
const isOverMax = props.count >= props.max;
...
}
当props发生变化时,组件会被重新计算,这就是原因。使用useEffect时,会发生以下无用的计算流程:props变化→重新计算→渲染→useEffect→state变化→重新计算→渲染
Option:
方法二:在数据提取过程中使用钩子函数。
提到,这与模式1相似,但是通过将其转化为hooks,可以减少不必要的Effect。useEffect在开头的官方文档中提到。
Effects是React范式的逃生口。它让您可以“走出去”,并将组件与非React小部件、网络或浏览器DOM等外部系统同步。
正如某些情况下所推荐的那样,在与网络进行通信时,可以使用以下代码作为示例。
import React, { useState, useEffect } from 'react';
import { fetchSomeData } from './api';
const ExampleComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetchSomeData();
setData(response.data);
};
fetchData();
}, []);
...
};
在这个例子中,我们需要使用axios或其他手段来获取数据,然后使用useEffect来确保结果能够被正确显示。
在miHub中,我们最近创建了一个名为useFetch的hooks,用于抽象化数据获取过程。
这个hooks的内部执行了以下类似的操作。
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
};
利用这个hooks,最初的代码可以改成如下:
const ExampleComponent = () => {
const { data, isLoading, error } = useFetch('/some-endpoint');
...
};
当收到回应后,由于hooks内部的state发生了变化,因此即使在组件内部没有使用useEffect,数据也会与通信结果保持同步。
如果可以在事件处理程序内进行处理,那么选择模式3。
在React组件中,当状态发生变化时,经常使用useEffect hook来执行某些处理。
import React, { useState, useEffect } from 'react';
const ExampleComponent = () => {
const [count, setCount] = useState<number>(0);
const [message, setMessage] = useState<string>('');
useEffect(() => {
setMessage('count has changed to', count);
}, [count]);
const handleButtonClick = () => {
setCount(count + 1);
};
...
};
然而,当状态发生变化时,您也可以在事件处理程序内直接执行所需的操作。
const ExampleComponent = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState<string>('');
const handleButtonClick = () => {
setCount(count + 1);
setMessage('count has changed to', count);
};
...
};
如果需要异步处理,则使用useEffect是合适的。但是,如果需要立即对用户的操作进行处理,则可以将代码写在事件处理程序中,从而减少不必要的渲染。
模式4:将数据获取移至父组件。
在进行组件拆分时,如果将数据获取也整合到子组件中,可能会使其更加清晰。
// Parentコンポーネント
import React, { useEffect, useState } from 'react';
import Child from './Child';
const Parent = () => {
const [data, setData] = useState(null);
useEffect(() => {
someFunction()
}, [data]);
const handleChildData = (childData) => {
setData(childData);
}
return (
<>
<Child onData={handleChildData} data={data} />
...
</>
)
}
// Childコンポーネント
import React, { useEffect, useState } from 'react';
const Child = ({ onData, data }) => {
const { request } = useRequest('get', 'https://api.example.com/child-data');
const onClick = async () => {
const response = await request();
onData(response.data);
}
return (
<>
<Button onClick={onClick}>get!</Button>
<div>{data}</div>
</>
)
}
我們有這樣的一個實現方法,但是這樣做的話。
-
- 子コンポーネントでデータ取得
-
- 親のsetterに渡す
-
- stateの変化で親子ともに再レンダリング
-
- 親はuseEffectでデータの取得を検知
- 処理の結果また必要に応じて再レンダリング
运行中会执行无用的重新计算处理。
此外,数据流也不容易追踪,容易产生错误状况。
// Parentコンポーネント
import React, { useEffect, useState } from 'react';
import Child from './Child';
const Parent = () => {
const [data, setData] = useState(null);
useEffect(() => {
someFunction()
}, [data]);
const { request } = useRequest('get', 'https://api.example.com/child-data');
const onClick = async () => {
const response = await request();
setData(response.data);
}
return (
<>
<Child onClick={onClick} data={data} />
...
</>
)
}
// Childコンポーネント
import React, { useEffect, useState } from 'react';
const Child = ({ onClick, data }) => {
return (
<>
<Button onClick={onClick}>get!</Button>
<div>{data}</div>
</>
)
}
重构后,我们在父组件中编写数据获取的逻辑。通过在props中传递onClick来发送请求,使得处理逻辑保持不变。
-
- 親コンポーネントでデータ取得
- stateの変化で親子ともに再レンダリング
只需要两个步骤就可以完成处理,并且数据流变得更加清晰易懂。
总结
由于亲子数据的传递、基于通信的数据获取时间和屏幕绘制时间等交织复杂,因此要确保原有功能非常费力,但经过重构发现存在低效的实现。
在miHub中,虽然公式文件中还介绍了其他的例子,但我们基本上能够通过上述模式来消除不必要的Effect。