React中的useImperativeHandle是一个自定义Hooks,它允许我们向子组件暴露一个特定的接口,使得我们可以在父组件中通过ref对象调用子组件中的方法或属性。其基本用法是:
import { forwardRef, useImperativeHandle } from 'react';
const Child = forwardRef((props, ref) => {
const [value, setValue] = useState(0);
useImperativeHandle(ref, () => ({
increment() {
setValue(value + 1);
}
}));
return (
<div>
Value: {value}
</div>
);
});
const Parent = () => {
const childRef = useRef();
const handleClick = () => {
childRef.current.increment();
};
return (
<div>
<Child ref={childRef} />
<button onClick={handleClick}>Increment</button>
</div>
);
};
从上面的示例可以看出,useImperativeHandle接受两个参数:一个ref对象和一个函数,这个函数返回的对象会被合并到ref对象中。我们通常会把ref对象可调用的方法或就算是state值都传递给父组件所使用。
一、控制子组件暴露何种接口
useImperativeHandle的第二个参数是一个函数,它返回的对象会被合并到ref对象中。通过返回的对象不同,我们可以控制子组件暴露出何种接口。假设我们在父子组件间需要通过ref对象来控制子组件的2个按钮:“加1”和“减1”,那么我们可以在useImperativeHandle中返回一个对象,这个对象分别对应了“加1”和“减1”方法:
import { forwardRef, useImperativeHandle } from 'react';
const Child = forwardRef((props, ref) => {
const [value, setValue] = useState(0);
const increment = () => {
setValue((val) => val + 1);
};
const decrement = () => {
setValue((val) => val - 1);
};
useImperativeHandle(ref, () => ({
increment,
decrement
}));
return (
<div>
Value: {value}
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
});
const Parent = () => {
const childRef = useRef();
const handleIncrement = () => {
childRef.current.increment();
};
const handleDecrement = () => {
childRef.current.decrement();
};
return (
<div>
<Child ref={childRef} />
<button onClick={handleIncrement}>Increment</button>
<button onClick={handleDecrement}>Decrement</button>
</div>
);
};
这样,我们就可以通过一个ref对象控制子组件内部的状态,而不必在子组件外再定义一个状态管理逻辑了。
二、仅在某些值发生改变时更新接口
useImperativeHandle的第三个参数是一个数组,它的元素是用来判断哪些依赖发生改变后再更新useImperativeHandle的返回值的。如果值发生变化,那么useImperativeHandle函数就会重新执行。举个例子:
import { forwardRef, useImperativeHandle, useState } from 'react';
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Child');
useImperativeHandle(ref, () => ({
increment() {
console.log(count);
setCount(count + 1);
}
}), [count]);
console.log('Render Child');
return <div>{name}: {count}</div>;
});
const Parent = () => {
const childRef = useRef();
const handleClick = () => {
childRef.current.increment();
};
return (
<div>
<Child ref={childRef} />
<button onClick={handleClick}>Increment</button>
</div>
);
};
在上面的例子中,在子组件中我们使用了count变量,这个变量是我们想让useImperativeHandle关注的状态。当count变化时,useImperativeHandle函数将重新执行,并更新ref所引用的值,这样我们就不需要再自己手动重新定义一下。
三、控制React组件工作流
在某些情况下,我们需要控制组件的生命周期,避免不必要的state更新。我们可以把所有控制代码封装到useImperativeHandle函数中。例如:
import { forwardRef, useImperativeHandle, useState } from 'react';
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Child');
useImperativeHandle(ref, () => ({
increment() {
console.log(count);
setCount(count + 1);
}
}), [count]);
console.log('Render Child');
return <div>{name}: {count}</div>;
});
const Parent = () => {
const [showChild, setShowChild] = useState(false);
const childRef = useRef();
const handleClick = () => {
childRef.current.increment();
};
return (
<div>
<button onClick={() => setShowChild(!showChild)}>Toggle Child</button>
{showChild && <Child ref={childRef}/>}
<button onClick={handleClick}>Increment</button>
</div>
);
};
在上述代码中,我们向子组件暴露了一个increment方法,这个方法被父组件控制。当我们点击“Toggle Child”按钮时,子组件重新出现在DOM中,并且再次渲染。但是,我们应该只在子组件出现时重新渲染一次子组件,而不是每次父组件中的任何状态发生变化就更新一次子组件。这个问题可以通过使用useImperativeHandle来解决。实现方法如下:
import { forwardRef, useImperativeHandle, useState, useMemo } from 'react';
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Child');
useImperativeHandle(ref, () => ({
increment() {
console.log(count);
setCount(count + 1);
}
}), [count]);
console.log('Render Child');
return <div>{name}: {count}</div>;
});
const MemoChild = memo(Child);
const Parent = () => {
const [showChild, setShowChild] = useState(false);
const childRef = useRef();
const handleClick = () => {
childRef.current.increment();
};
const child = useMemo(() => <MemoChild ref={childRef}/>, []);
return (
<div>
<button onClick={() => setShowChild(!showChild)}>Toggle Child</button>
{showChild && child}
<button onClick={handleClick}>Increment</button>
</div>
);
};
在Parent组件中,我们使用了useMemo来 memorize Child组件,并通过 child变量渲染它。在父组件状态发生变化时,child不会被重新渲染,因为Child组件的props没有发生变化。只有在我们触发了“Toggle Child”按钮并且showChild和child之间的匹配状态发生变化时,child重新渲染。