引言
相信大部分同学对ref的认知还处于获取DOM
节点和组件实例层面上,实际上除了这个功能,还有其它小技巧可以使用,这篇文章将详细地介绍ref
的创建和使用,相关代码会以函数组件为主。
创建Ref
字符串类型Ref
class App extends React.Component {
render() {
console.log("this", this);
return (
<>
<div ref="dom">hello world</div>
<Children ref="children" />
</>
);
}
}
class Children extends React.Component {
render() {
return <div>china</div>;
}
}
打印结果
无论是对于真实DOM
,还是类组件,通过字符串创建的ref
会绑定在this.refs
对象上。
函数类型Ref
class App extends React.Component {
dom = null
children = null
componentDidMount() {
console.log(this.dom, this.childrenDom);
}
render() {
return (
<>
<div ref={(node) => this.dom = node}>hello world</div>
<Children ref={(node) => this.childrenDom = node} />
</>
);
}
}
class Children extends React.Component {
render() {
return <div>china</div>;
}
}
打印结果
对象类型Ref
createRef创建Ref
创建对象类型的Ref
对于类组件和函数组件的创建方式是不一样的,在类组件中创建Ref
对象需要通过createRef
函数来进行创建,这个函数的实现其实非常简单
function createRef() {
return {
current: null
}
}
从上面的代码来看,就是创建并返回带有current
属性的对象。
注意:不要在函数组件里面使用createRef函数
举个例子来看下为什么在函数组件里面不能使用createRef函数来创建Ref
function Counter() {
const ref1 = React.useRef(0);
const ref2 = React.createRef();
const [count, setCount] = React.useState(0);
console.log(ref1, ref2);
return (
<>
<div class="counter-area">
<div class="count1">count1: {ref1.current}</div>
<div class="count2">count2: {ref2.current}</div>
</div>
<button
onClick={() => {
ref1.current = (ref1.current || 0) + 1;
ref2.current = (ref2.current || 0) + 1;
setCount(count + 1);
}}
>
点我++
</button>
</>
);
}
一起看下打印结果
发现count2
的右侧一直没有打印,根据控制台里面打印的数据不难发现由createRef
创建的ref
的current
属性一直都为null
,所以count2
右侧没有数据。
函数组件更新UI视图实际上会将整个函数重新执行一遍,那么createRef
函数在组件每次渲染的时候也会重新调用,生成初始状态的ref
,对应的current
属性为null
。
useRef创建Ref
React
提供了内置的useRef
来生成ref
对象。
function Counter() {
const ref = React.useRef(null);
React.useEffect(() => {
console.log(ref.current);
}, []);
return <div ref={ref}>hello world</div>;
}
上面有说过在函数组件里面通过createRef
创建ref
会有问题,那通过useRef
这个函数生成的ref
会不会有上述的问题?
答案是不会,在类组件里面可以把ref
存储到实例对象上去,但是函数组件并没有实例的说法,但是函数组件有对应的Fiber
对象,只要组件没有销毁,那么fiber
对象也不会销毁,将useRef
产生的ref
挂载到对应的fiber
对象上,那么ref
就不会在函数每次重新执行时被重置掉。
Ref的高阶用法
Ref转发
我们在通过Props的方式向组件传递信息时,某些特定的属性是会被React
底层处理的,我们在组件里面是无法接受到的,例如key
、ref
function Counter(props) {
const { key, ref } = props;
console.log("props", props);
return (
<>
<div class="key">key: {key}</div>
<div class="ref">ref: {ref.current}</div>
</>
);
}
function App() {
const ref = useRef(null);
return <Counter key={"hello"} ref={ref} />;
}
控制台中打印信息如下,可以了解到通过props
的方式传递key
和ref
给组件是无效的。
那如何传递ref
和key
给子组件,既然react
不允许,那么我们换个身份,暗度陈仓。
function Counter(props) {
const { pkey, pref } = props;
console.log("props", props);
return (
<>
<div class="key">key: {pkey}</div>
<div class="ref">ref: {pref.current}</div>
</>
);
}
function App() {
const ref = useRef(null);
return <Counter pkey={"hello"} pref={ref} />;
}
打印信息如下
通过别名的方式可以传递ref,那么为什么还需要forwardRef来传递Ref?
假设这样一种场景,你的组件中需要引用外部库提供的组件,例如antd
、fusion
这种组件库提供的组件,而且你需要传递ref
给这个组件,因为引用的外部组件是不可能知道你会用什么样的别名属性来传递ref
,所以这个时候只能使用forwardRef
来进行传递ref
,因为这个是react
规定用来传递ref
值的属性名。
跨层级传递Ref
场景:需要把ref
从GrandFather
组件传递到Son
组件,在Son
组件里面展示ref
存储的信息并获取Son
组件里面的dom
节点。
const Son = (props) => {
const { msg, grandFatherRef } = props;
return (
<>
<div class="msg">msg: {msg}</div>
<div class="ref" ref={grandFatherRef}>
ref: {grandFatherRef.current}
</div>
</>
);
};
const Father = forwardRef((props, ref) => {
return <Son grandFatherRef={ref} {...props} />;
});
function GrandFather() {
const ref = useRef("我是来自GrandFather的ref啦~~");
useEffect(() => console.log(ref.current), []);
return <Father ref={ref} msg={"我是来自GrandFather的消息啦~~"} />;
}
页面展示情况
控制台打印结果
上面的代码就是通过别名配合forwardRef
来转发ref
,这种转发ref
的方式,是非常常见的用法。通过别名的方式传递ref
和通过forwardRef
传递ref
的方式其实没有太大的差别,最本质的区别就是通过别名的方式需要传递方和接收方人为地都约定好属性名,而通过forwardRef
的方式是react
里面约定了传递的ref
属性名。
合并转发Ref
我们从父组件传递下去的ref不仅仅可以用来展示获取某个dom
节点,还可以在子组件里面更改ref
的信息,获取子组件里面其它信息。
场景:我们需要获取子组件里面input和button这两个dom
节点
const Son = forwardRef((props, ref) => {
console.log("props", props);
const sonRef = useRef({});
useEffect(() => {
ref.current = {
...sonRef.current,
};
return () => ref.current = {}
}, []);
return (
<>
<button ref={(button) => Object.assign(sonRef.current, { button })}>
点我
</button>
<input
type="text"
ref={(input) => Object.assign(sonRef.current, { input })}
/>
</>
);
});
function Father() {
const ref = useRef("我是来自Father的ref啦~~");
useEffect(() => console.log(ref.current), []);
return <Son ref={ref} msg={"我是来自Father的消息啦~~"} />;
}
控制台打印结果
尽管我们传递下去的ref
的current
为字符串属性,但是我们通过在子组件里面修改current
属性,进而获取到了子组件里面button
和input
的dom
节点
上面的代码其实可以把useEffect
更改成为useImperativeHandle
,这是react
提供的hook
,用来配合forwardRef
来进行使用,可以。
useImperativeHandle
接收三个参数:
- 通过
forwardRef
传递过来的ref
- 处理函数,其返回值暴露给父组件的
ref
对象 依赖项
const Son = forwardRef((props, ref) => { console.log("props", props); const sonRef = useRef({}); useImperativeHandle( ref, () => ({ ...sonRef.current, }), [] ); return ( <> <button ref={(button) => Object.assign(sonRef.current, { button })}> 点我 </button> <input type="text" ref={(input) => Object.assign(sonRef.current, { input })} /> </> ); }); function Father() { const ref = useRef("我是来自Father的ref啦~~"); useEffect(() => console.log(ref.current), []); return <Son ref={ref} msg={"我是来自Father的消息啦~~"} />; }
高阶组件转发Ref
在使用高阶组件包裹一个原始组件的时候,因为高阶组件会返回一个新的组件,如果不进行ref转发,从上层组件传递下来的ref会指向这个新的组件
function HOC(Comp) { function Wrapper(props) { const { forwardedRef, ...restProps } = props; return <Comp ref={forwardedRef} {...restProps} />; } return forwardRef((props, ref) => ( <Wrapper forwardedRef={ref} {...props} /> )); } const Son = forwardRef((props, ref) => { return <div ref={ref}>hello world</div>; }); const NewSon = HOC(Son); function Father() { const ref = useRef("我是来自Father的ref啦~~"); useEffect(() => console.log(ref.current), []); return <NewSon ref={ref} msg={"我是来自Father的消息啦~~"} />; }
注意:第12行处增加
forwardRef
转发是因为函数组件无法直接接收ref
属性
控制台打印结果
Ref实现组件间的通信
子组件可以通过props
获取和改变父组件里面的state
,父组件也可以通过ref
来获取和改变子组件里面的state
,实现了父子组件之间的双向通信,这其实在一定程度上打破了react单向数据传递的原则。
const Son = forwardRef((props, ref) => {
const { toFather } = props;
const [fatherMsg, setFatherMsg] = useState('');
const [sonMsg, setSonMsg] = useState('');
useImperativeHandle(
ref,
() => ({
fatherSay: setFatherMsg
}),
[]
);
return (
<div className="son-box">
<div className="father-say">父组件对我说: {fatherMsg}</div>
<div className="son-say">
我对父组件说:
<input
type="text"
value={sonMsg}
onChange={(e) => setSonMsg(e.target.value)}
/>
<button onClick={() => toFather(sonMsg)}>to father</button>
</div>
</div>
);
});
function Father() {
const [fatherMsg, setFatherMsg] = useState('');
const [sonMsg, setSonMsg] = useState('');
const sonRef = useRef(null);
return (
<div className="father-box">
<div className="son-say">子组件对我说: {sonMsg}</div>
<div className="father-say">
对子组件说:
<input
type="text"
value={fatherMsg}
onChange={(e) => setFatherMsg(e.target.value)}
/>
<button onClick={() => sonRef.current.fatherSay(fatherMsg)}>
to son
</button>
</div>
<Son ref={sonRef} toFather={setSonMsg} />
</div>
);
}
Ref和State的抉择
我们在平常的学习和工作中,在使用函数组件的时候,常常会使用useState
来生成state
,每次state
改变都会引发整个函数组件重新渲染,但是有些state
的改变不需要更新视图,那么我们可以考虑使用ref来存储,ref
存储的值在发生变化后是不会引起整个组件的重新渲染的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。