通过示例讲解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。

广告
将在 10 秒后关闭
bannerAds