使用 useImperativeHandle 修改子组件的 state
April 10, 2021
前言
在 React 的开发理念中,一切皆由组件渲染,而组件数据分为两大来源:state 和 props.
state 指组件自身内部的数据,props 指父组件传递给子组件的数据。
严格来说,props 也属于 state,因为总有一个最顶级父组件持有 state,当传递至子组件时,也就变成了 "props".
一般情况下,子组件的 state 只在该组件内部被使用,但如果父组件需要改变子组件的 state,又该如何是好?
如何修改子组件的 state
举个 🌰 例子:
子组件负责渲染 label 和 input,并处理受控组件的文字输入。
tsx
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>);};
tsx
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" 后向服务端发送请求。
tsx
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>);}
tsx
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 共享。
tsx
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>);};
tsx
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>);};
tsx
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 fieldsetVal("");};return (<div className="App"><Child onChange={handleChange} val={val} setVal={setVal}><span>Please Enter your name</span></Child> <button onClick={handleSend}>send</button></div>);}
tsx
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 fieldsetVal("");};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
方法,它的函数签名如下:
tsx
useImperativeHandle(ref, createHandle, [deps]);
tsx
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 这样的命令式代码,但此情此景,非它莫属。
具体看下面的代码示例:
tsx
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 fieldchildRef.current?.emptyField();};return (<div className="App"><Child onChange={handleChange} ref={childRef}><span>Please Enter your name</span></Child> <button onClick={handleSend}>send</button></div>);}
tsx
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 fieldchildRef.current?.emptyField();};return (<div className="App"><Child onChange={handleChange} ref={childRef}><span>Please Enter your name</span></Child> <button onClick={handleSend}>send</button></div>);}