等效于使用 React 钩子进行组件 DidUpdate

如何模拟 componentDidUpdate或使用带有数组的 key支撑来强制我的组件被重置?

我实现了一个组件,它显示一个计时器,当它达到零时执行一个回调。回调的目的是更新对象列表。后者由新的 反应钩 useStateuseEffect组成。

state包含对计时器启动时间和剩余时间的引用。effect设置每秒调用一次的时间间隔,以更新剩余的时间,并检查是否应该调用回调。

该组件并不意味着重新调度计时器,或者在间隔为零时继续运行,它应该执行回调和空闲。为了让计时器刷新,我希望传递一个数组到 key,这将导致组件的状态被重置,因此计时器将重新启动。不幸的是,key必须与字符串一起使用,因此无论数组引用是否更改,都不会产生任何效果。

我还试图通过传递我所关心的数组来推动道具的更改,但是状态得到了维护,因此间隔没有重置。

观察数组中浅层变化以强制仅使用新钩子 API 更新状态的首选方法是什么?

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';


function getTimeRemaining(startedAt, delay) {
const now = new Date();
const end = new Date(startedAt.getTime() + delay);
return Math.max(0, end.getTime() - now.getTime());
}


function RefresherTimer(props) {
const [startedAt, setStartedAt] = useState(new Date());
const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay));


useEffect(() => {


if (timeRemaining <= 0) {
// The component is set to idle, we do not set the interval.
return;
}


// Set the interval to refresh the component every second.
const i = setInterval(() => {
const nowRemaining = getTimeRemaining(startedAt, props.delay);
setTimeRemaining(nowRemaining);


if (nowRemaining <= 0) {
props.callback();
clearInterval(i);
}
}, 1000);


return () => {
clearInterval(i);
};
});


let message = `Refreshing in ${Math.ceil(timeRemaining / 1000)}s.`;
if (timeRemaining <= 0) {
message = 'Refreshing now...';
}


return <div>{message}</div>;
}


RefresherTimer.propTypes = {
callback: PropTypes.func.isRequired,
delay: PropTypes.number
};


RefresherTimer.defaultProps = {
delay: 2000
};


export default RefresherTimer;

试图与 key一起使用:

<RefresherTimer delay={20000} callback={props.updateListOfObjects} key={listOfObjects} />

试图用道具改变:

<RefresherTimer delay={20000} callback={props.updateListOfObjects} somethingThatChanges={listOfObjects} />

listOfObjects指的是一个对象数组,其中对象本身不一定会发生变化,因此应该将该数组与 !==进行比较。通常,该值将来自 Redux,其中的操作 updateListOfObjects使数组像这样重新初始化: newListOfObjects = [...listOfObjects]

88835 次浏览

A way to remount a component is to provide new key property. It's not necessarily a string but it will be coerced to a string internally, so if listOfObjects is a string, it's expected that key is compared internally with listOfObjects.toString().

Any random key can be used, e.g. uuid or Math.random(). Shallow comparison of listOfObjects can be performed in parent component to provide new key. useMemo hook can be used in parent state to conditionally update remount key, and listOfObjects can be used as a list of parameters that need to be memoized. Here's an example:

  const remountKey = useMemo(() => Math.random(), listOfObjects);


return (
<div>
<RefresherTimer delay={3000} callback={() => console.log('refreshed')} key={remountKey} />
</div>
);

As an alternative to remount key, child component could be able to reset own state and expose a callback to trigger a reset.

Doing shallow comparison of listOfObjects inside child component would be an antipattern because this requires it to be aware of parent component implementation.

In short, you want to reset your timer when the reference of the array changes, right ? If so, you will need to use some diffing mechanism, a pure hooks based solution would take advantage of the second parameter of useEffect, like so:

function RefresherTimer(props) {
const [startedAt, setStartedAt] = useState(new Date());
const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay));


//reset part, lets just set startedAt to now
useEffect(() => setStartedAt(new Date()),
//important part
[props.listOfObjects] // <= means: run this effect only if any variable
// in that array is different from the last run
)


useEffect(() => {
// everything with intervals, and the render
})
}

More information about this behaviour here https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

The useRef creates an "instance variable" in functional component. It acts as a flag to indicate whether it is in mount or update phase without updating state.

const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
// do componentDidMount logic
mounted.current = true;
} else {
// do componentDidUpdate logic
}
});

use a custom hook

export const useComponentDidUpdate = (effect, dependencies) => {
const hasMounted = useRef(false);


useEffect(
() => {
if (!hasMounted.current) {
hasMounted.current = true;
return;
}
effect();
},
dependencies
);
};


Effect will not run after the initial render. Thereafter, it depends on the array of values that should be observed. If it's empty, it will run after every render. Otherwise, it will run when one of it's values has changed.

Create hook first

function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}

Now in the main function

import React, {useEffect, useState} from 'react';
import {Text, View} from 'react-native';
import usePrevious from './usePrevious';


export default function Example() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
  



useEffect(() => {
// this one is your didupdate method for count variable
if (count != prevCount) {
alert('count updated')
}
}, [count]);






return (
<View>
<Text>
You clicked {count} times {prevCount}{' '}
</Text>
      

<Text onPress={() => setCount(count + 1)}>Increment</Text>


<Text onPress={() => setCount(count - 1)}>Decrement</Text>
</View>
);
}

You can use useUpdateEffect from react-use.

Universal TypeScript version:

import { DependencyList, useEffect, useRef } from "react"


type Destructor = () => void
type MountEffectCallback = (firstLoad: boolean) => (void | Destructor)


export const useDidUpdateEffect = (effect: MountEffectCallback, deps: DependencyList) => {
const firstLoad = useRef(true)


useEffect(() => {
effect(firstLoad.current)


if (firstLoad.current) {
firstLoad.current = false
}
}, deps)
}