useImperativeHandle方式 vs useState方式

huangapple go评论79阅读模式
英文:

useImperativeHandle way vs useState way

问题

React建议首选第一种解决方案(使用状态而不是useImperativeHandle,只在必要时使用),因为它更符合React的设计哲学和组件封装原则。以下是一些关于为什么React可能更推荐第一种解决方案的原因:

  1. 可维护性和可理解性: 第一种方法使用了父组件传递状态和回调函数的方式,使得组件之间的数据流更加清晰和可理解。这有助于提高代码的可维护性,因为数据和逻辑更容易追踪和调试。

  2. 组件复用: 第一种方法更容易实现组件的重用。您可以将CountDownTimer组件在应用的其他部分使用,而不需要传递ref或者特定的函数。

  3. React的声明性: React推崇声明性编程,其中组件的行为由其props和状态决定。第一种方法更符合这个原则,因为它明确地传递了计时器的值和逻辑,而不是通过ref来操作组件的内部状态。

尽管第二种方法可以将组件内部的逻辑封装得更好,但它可能使代码复杂化,增加了不必要的复杂性。通常情况下,React建议只在必要时使用useImperativeHandle,例如在与非React库(如D3或Three.js)集成时,或者在必须以编程方式控制子组件的情况下使用。在大多数情况下,使用props和状态管理组件的行为更加合适和清晰。

英文:

I have a CountDownTimer component. This component should be able to get a current countDownTimerValue from the parent component and start the timer that way. The parent component disables some components if the countdown has started. This can be implemented in two ways:

  1. using states and passing them from the parent component
  2. using the useImperativeHandle to pass some functions from the CountDownTimer component

The first implementation looks like this:
CountDownComponent:

import React, { useEffect, useRef } from "react";

export type Props = {
  seconds: number;
  decreaseCountDown: () => void;
};

export const isCountDownTimerVisible = (currentTimer: number) => {
  return currentTimer > MIN_TIMER_VALUE;
}

export const MIN_TIMER_VALUE = -1;

const CountDownTimer = ({ seconds, decreaseCountDown }: Props) => {
  const intervalId = useRef<NodeJS.Timer | undefined>(undefined);

  useEffect(() => {
    createIntervalIfRequired();
    return () => clearIntervalIfRequired();
  }, [seconds]);

  const createIntervalIfRequired = () => {
    if (intervalId.current !== undefined) {
      return;
    }
    if (seconds <= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() => {
      decreaseCountDown();
    }, 1000);

    intervalId.current = createdIntervalId;
  };
  
  const clearIntervalIfRequired = () => {
    if (seconds > MIN_TIMER_VALUE || intervalId.current == undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return (
    <span className={`px-2 ${seconds <= MIN_TIMER_VALUE ? "hidden" : ""}`}>
      {beautifyTime(seconds)}
    </span>
  );
};

const beautifyTime = (time: number): string => {
  let minutes = parseInt((time / 60).toString()).toString();
  let seconds = parseInt((time % 60).toString()).toString();

  if (seconds.length == 1) {
    seconds = "0" + seconds;
  }

  if (minutes.length == 1) {
    minutes = "0" + minutes;
  }

  return `${minutes}:${seconds}`;
};

export default CountDownTimer;

By doing this the parent component should hold the current timer and the logic to decrease the value for this component which I think is something internal to this component and I don't like that this is something that is created within the parent component.

Parent component:

 // Other code ...
  {!isCountDownTimerVisible(countDownTimerValue) && t("Send pin2 Request")}
  <CountDownTimer
    seconds={countDownTimerValue}
    decreaseCountDown={() => {
      if (sendPin2Status == "error") {
        setCountDownTimer(MIN_TIMER_VALUE);
        return;
      }
      setCountDownTimer((currentCounter) => currentCounter - 1);
    }}
  />

Now let's take a look at the imperative way.
CountDownComponent:

import React, {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState
} from "react";

export type CountDownTimerApi = {
  isVisible: () => boolean;
  setTimer: (seconds: number) => void;
};

export type Props = {
  ref: React.MutableRefObject<CountDownTimerApi>;
};

export const isCountDownTimerVisible = (currentTimer: number) => {
  return currentTimer > MIN_TIMER_VALUE;
}

export const MIN_TIMER_VALUE = -1;

const CountDownTimer = forwardRef(function CountDownTimer({ ref }: Props) {
  const intervalId = useRef<NodeJS.Timer | undefined>(undefined);
  const [countDownTimer, setCountDownTimer] = useState<number>(-1);

  useImperativeHandle(
    ref,
    () => {
      return {
        isVisible: () => {
          return countDownTimer >= MIN_TIMER_VALUE;
        },

        setTimer: (seconds: number) => {
          setCountDownTimer(seconds);
        }
      };
    },
    [countDownTimer]
  );

  useEffect(() => {
    createIntervalIfRequired();
    return () => clearIntervalIfRequired();
  }, [countDownTimer]);

  const createIntervalIfRequired = () => {
    if (intervalId.current !== undefined) {
      return;
    }
    if (countDownTimer <= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() => {
      setCountDownTimer((previousValue) => previousValue - 1);
    }, 1000);

    intervalId.current = createdIntervalId;
  };

  const clearIntervalIfRequired = () => {
    if (countDownTimer > MIN_TIMER_VALUE || intervalId.current === undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return (
    <span className={`px-2 ${countDownTimer <= MIN_TIMER_VALUE ? "hidden" : ""}`}>
      {beautifyTime(countDownTimer)}
    </span>
  );
});

const beautifyTime = (time: number): string => {
  let minutes = parseInt((time / 60).toString()).toString();
  let seconds = parseInt((time % 60).toString()).toString();

  if (seconds.length === 1) {
    seconds = "0" + seconds;
  }

  if (minutes.length === 1) {
    minutes = "0" + minutes;
  }

  return `${minutes}:${seconds}`;
};

export default CountDownTimer;

By doing this everything internal to the component is encapsulated within the component. This I like. But the react docs suggests to not use this hook and use states if possible.

Parent component:

  // other code ...
  {!countDownTimerRef.current?.isVisible() && t("Send pin2 Request")}
  <CountDownTimer ref={countDownTimerRef}/>

Why does React suggest the first solution (using states instead of useImperativeHandle whenever possible, see Pitfalls section) whilst the second one encapsulates everything related to the component within the component?

答案1

得分: 1

The primary reason why React advises against useImperativeHandle whenever possible is because it goes against one of the core philosophies of React: data flow should be explicit and easy to follow. useImperativeHandle breaks this convention by allowing parent components to access and manipulate the state of its children directly, bypassing the normal unidirectional data flow.

This may not be a problem for small projects or if used sparingly, but for large projects with multiple developers, it could potentially lead to code that is difficult to understand and maintain. When data changes happen, it would be hard to trace where the changes come from. It could also lead to bugs that are difficult to track down, since any part of the code that has access to the ref can modify it.

On the other hand, when using state and props, the data flow is clear and unidirectional: data is passed down from parent to child through props and changes in child components are communicated to the parent through callbacks.

In your particular case, using useImperativeHandle seems to lead to cleaner code since it keeps everything related to the countdown timer inside the CountDownTimer component. This is a valid consideration, and there are definitely cases where useImperativeHandle can be the better choice. However, even without useImperativeHandle, you could still achieve a similar degree of encapsulation by creating a custom hook that handles all the timer-related logic:

function useCountdown(seconds) {
  const [countDownTimer, setCountDownTimer] = useState(seconds);
  const intervalId = useRef<NodeJS.Timer | undefined>(undefined);

  useEffect(() => {
    createIntervalIfRequired();
    return () => clearIntervalIfRequired();
  }, [countDownTimer]);

  const createIntervalIfRequired = () => {
    if (intervalId.current !== undefined) {
      return;
    }
    if (countDownTimer <= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() => {
      setCountDownTimer((previousValue) => previousValue - 1);
    }, 1000);

    intervalId.current = createdIntervalId;
  };

  const clearIntervalIfRequired = () => {
    if (countDownTimer > MIN_TIMER_VALUE || intervalId.current === undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return [countDownTimer, setCountDownTimer];
}

You can use this hook in your CountDownTimer component as shown in the code you provided.

英文:

The primary reason why React advises against useImperativeHandle whenever possible is because it goes against one of the core philosophies of React: data flow should be explicit and easy to follow. useImperativeHandle breaks this convention by allowing parent components to access and manipulate the state of its children directly, bypassing the normal unidirectional data flow.

This may not be a problem for small projects or if used sparingly, but for large projects with multiple developers, it could potentially lead to code that is difficult to understand and maintain. When data changes happen, it would be hard to trace where the changes come from. It could also lead to bugs that are difficult to track down, since any part of the code that has access to the ref can modify it.

On the other hand, when using state and props, the data flow is clear and unidirectional: data is passed down from parent to child through props and changes in child components are communicated to the parent through callbacks.

In your particular case, using useImperativeHandle seems to lead to cleaner code since it keeps everything related to the countdown timer inside the CountDownTimer component. This is a valid consideration, and there are definitely cases where useImperativeHandle can be the better choice.However, even without useImperativeHandle, you could still achieve a similar degree of encapsulation by creating a custom hook that handles all the timer-related logic:

function useCountdown(seconds) {
  const [countDownTimer, setCountDownTimer] = useState(seconds);
  const intervalId = useRef&lt;NodeJS.Timer | undefined&gt;(undefined);

  useEffect(() =&gt; {
    createIntervalIfRequired();
    return () =&gt; clearIntervalIfRequired();
  }, [countDownTimer]);

  const createIntervalIfRequired = () =&gt; {
    if (intervalId.current !== undefined) {
      return;
    }
    if (countDownTimer &lt;= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() =&gt; {
      setCountDownTimer((previousValue) =&gt; previousValue - 1);
    }, 1000);

    intervalId.current = createdIntervalId;
  };

  const clearIntervalIfRequired = () =&gt; {
    if (countDownTimer &gt; MIN_TIMER_VALUE || intervalId.current === undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return [countDownTimer, setCountDownTimer];
}

Edit based on comment :

useCountdown hook can be used in the CountDownTimer component. also adding an isVisible function to the return value of useCountdown, to check if the timer is visible or not:

function useCountdown(initialSeconds, onCountdownEnd) {
  const [countDownTimer, setCountDownTimer] = useState(initialSeconds);
  const intervalId = useRef&lt;NodeJS.Timer | undefined&gt;(undefined);

  useEffect(() =&gt; {
    createIntervalIfRequired();
    return () =&gt; clearIntervalIfRequired();
  }, [countDownTimer]);

  const createIntervalIfRequired = () =&gt; {
    if (intervalId.current !== undefined) {
      return;
    }
    if (countDownTimer &lt;= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() =&gt; {
      setCountDownTimer((previousValue) =&gt; {
        if (previousValue - 1 &lt;= MIN_TIMER_VALUE) {
          onCountdownEnd(); // invoke the callback when timer reaches the minimum value
        }
        return previousValue - 1;
      });
    }, 1000);

    intervalId.current = createdIntervalId;
  };

  const clearIntervalIfRequired = () =&gt; {
    if (countDownTimer &gt; MIN_TIMER_VALUE || intervalId.current === undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  const isVisible = () =&gt; {
    return countDownTimer &gt; MIN_TIMER_VALUE;
  };

  return {countDownTimer, setCountDownTimer, isVisible};
}

You can then use this hook in your CountDownTimer component like this:

const CountDownTimer = ({ initialSeconds, onCountdownEnd }) =&gt; {
  const { countDownTimer, setCountDownTimer, isVisible } = useCountdown(initialSeconds, onCountdownEnd);

  useEffect(() =&gt; {
    setCountDownTimer(initialSeconds);
  }, [initialSeconds]);

  return (
    &lt;span className={`px-2 ${!isVisible() ? &quot;hidden&quot; : &quot;&quot;}`}&gt;
      {beautifyTime(countDownTimer)}
    &lt;/span&gt;
  );
};

Here, CountDownTimer receives initialSeconds as a prop, and passes it to useCountdown. The useEffect hook in CountDownTimer ensures that the timer is reset whenever initialSeconds changes. The isVisible function is used to control the visibility of the timer.

The parent component would look like this:

const ParentComponent = () =&gt; {
  const [initialSeconds, setInitialSeconds] = useState(60);
  const [countDownVisible, setCountDownVisible] = useState(true); // Add a new state for the visibility

  const handleCountdownEnd = () =&gt; {
    // This function will be called when the countdown ends
    setCountDownVisible(false); // Hide the countdown when it ends
  };

  return (
    &lt;div&gt;
      {/* Other code... */}
      {countDownVisible &amp;&amp; &lt;CountDownTimer initialSeconds={initialSeconds} onCountdownEnd={handleCountdownEnd}/&gt;}
    &lt;/div&gt;
  );
};

In this case, ParentComponent manages initialSeconds which controls the start value of the countdown. When it wants to start a countdown, it can just update initialSeconds, and CountDownTimer will start counting down from that value. Note that initialSeconds doesn't need to be state - it could also be a prop, or calculated based on other values. The important part is that it controls the start value of the countdown.

huangapple
  • 本文由 发表于 2023年8月5日 04:21:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76838918.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定