前言
在 React 的开发理念中,一切皆由组件渲染,而组件数据分为两大来源:state 和 props.
state 指组件自身内部的数据,props 指父组件传递给子组件的数据。
严格来说,props 也属于 state,因为总有一个最顶级父组件持有 state,当传递至子组件时,也就变成了 "props".
一般情况下,子组件的 state 只在该组件内部被使用,但如果父组件需要改变子组件的 state,又该如何是好?
如何修改子组件的 state
举个 🌰 例子:
子组件负责渲染 label 和 input,并处理受控组件的文字输入。
interface IChild {
onChange: (val: string) => void;
}
const Child: FC<IChild> = ({ onChange, children }) => {
const [val, setVal] = useState("");
const handleInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
const { value } = ev.target;
setVal(value);
onChange(value);
};
return (
<label>
{children}:
<input value={val} onChange={handleInputChange} />
</label>
);
};
父组件负责渲染 button,拿到 name 后向服务端发送请求。
export default function Parent() {
const [name, SetName] = useState("");
const handleChange = (val: string) => {
SetName(val);
};
const handleSend = () => {
console.log(`your name: ${name} will be sent`);
// fakePost(someUrl, name)
SetName("");
// next we how to empty Child's input field
};
return (
<div className="App">
<Child onChange={handleChange}>
<span>Please Enter your name</span>
</Child>
<button onClick={handleSend}>send</button>
</div>
);
}
注意,当请求被发送后,父组件需要清空输入框的值。
由于子组件中的 input 是受控组件,父组件必须要改变子组件内部 state,即 name 的值。
lift state up(状态提升)
如果你使用过 Redux 等全局状态管理工具,会很自然想到 Context 的概念,将子组件的 state 提升至父组件中的 state(Context)进行管理,父组件再将 state 作为 props 传递至子组件。
这么做的目的是只在父组件维护一份 state,父子组件实现 state 共享。
interface IChild {
val: string;
setVal: (newVal: string) => void;
onChange: (val: string) => void;
}
const Child: FC<IChild> = ({ onChange, children, val, setVal }) => {
const handleInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
const { value } = ev.target;
setVal(value);
onChange(value);
};
return (
<label>
{children}:
<input value={val} onChange={handleInputChange} />
</label>
);
};
export default function Parent() {
const [name, SetName] = useState("");
const [val, setVal] = useState("");
const handleChange = (val: string) => {
SetName(val);
};
const handleSend = () => {
console.log(`your name: ${name} will be sent`);
// fakePost(someUrl, name)
SetName("");
// empty Child's input field
setVal("");
};
return (
<div className="App">
<Child onChange={handleChange} val={val} setVal={setVal}>
<span>Please Enter your name</span>
</Child>
<button onClick={handleSend}>send</button>
</div>
);
}
在线例子:CodeSandbox - lift state up
状态提升虽然实现简单、易理解,但也存在弊端:
<Child />组件和父组件强耦合setName和setVal两个 useState 显得冗余
useImperativeHandle(命令式获取)
在这种情形下,我比较倾向于使用 React Hooks 中的 useImperativeHandle 方法,它的函数签名如下:
useImperativeHandle(ref, createHandle, [deps]);
官网上是这么描述的,useImperativeHandle 可以让你在使用 ref 时 自定义暴露给父组件的实例值。
上面那句话比较绕口,通俗来说,ref 在 React 中实际上是一个可变对象(ref.current),他除了可以传递给原生的 HTML 标签以访问 DOM,还可以搭配 forwardRef 传递给子组件,子组件转发 ref 到其内部的 HTML 标签,从而将 DOM 对象赋值给 ref.current.
有了 useImperativeHandle 的存在,ref.current 可以不仅仅被赋值为一个 DOM 对象,还可以是一个普通(字面量)对象,对象里的方法,可以访问子组件的任意变量(上下文)。
于是父组件就能通过 ref.current.customMethod() 的方式修改子组件的 state.
虽然官方提倡在大多数情况下,应当避免使用 ref 这样的命令式代码,但此情此景,非它莫属。
具体看下面的代码示例:
import React, {
useState,
useImperativeHandle,
forwardRef,
useRef,
} from "react";
interface IChild {
onChange: (val: string) => void;
children: React.ReactNode;
}
interface IRef {
emptyField(): void;
}
const Child = forwardRef<IRef, IChild>(({ onChange, children }, ref) => {
const [val, setVal] = useState("");
useImperativeHandle(
ref,
() => ({
emptyField() {
setVal("");
},
}),
[]
);
const handleInputChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
const { value } = ev.target;
setVal(value);
onChange(value);
};
return (
<label>
{children}:
<input value={val} onChange={handleInputChange} />
</label>
);
});
export default function Parent() {
const [name, SetName] = useState("");
const childRef = useRef<IRef>(null);
const handleChange = (val: string) => {
SetName(val);
};
const handleSend = () => {
console.log(`your name: ${name} will be sent`);
// fakePost(someUrl, name)
SetName("");
// empty Child's input field
childRef.current?.emptyField();
};
return (
<div className="App">
<Child onChange={handleChange} ref={childRef}>
<span>Please Enter your name</span>
</Child>
<button onClick={handleSend}>send</button>
</div>
);
}