第九章 Refs:从存储数据到指令式API 中

195 阅读3分钟

文章出处:www.advanced-react.com/

专栏地址:juejin.cn/column/7443…

Ref更新是同步且可变的

第二个不同在于,Ref的更新是同步的。更新Ref不过是在改变一个对象罢了,而这个动作在JavaScript中是同步的。但是更新状态(state),则是异步的。在React中更新状态(state)甚至更复杂:状态更新是发生在“快照”里的。它有一个复杂的系统来管理状态,来确保一个“快照”中的数据和组件是连续的、被适度更新的。Ref呢,则没有那么复杂:我们只是直接改变一个对象就行。

通过下面这个例子,我们就明白了:

const Form = () => {
    const [value, setValue] = useState();
    
    const onChange = (e) => {
        console.log('before', value);
        setValue(e.target.value);
        console.log('after', value); // same as before
    }
}

前值和后值是一样的。我们调用setvalue时,React不会立即更新这个状态。我们只是让React知道,在它完成当前正在进行的操作之后,需要用新的数据来安排一次状态更新。

而更新Ref,则相反:

const Form = () => {
    const ref = useRef()
    
    const onChange = (e) => {
        console.log('before', value);
        ref.current = e.target.value
        console.log('after', value); // already changed
    }
}

我们修改了一个对象,该对象中的数据当下就能获取到,但不会触发 React 生命周期中的任何操作。

我们可以使用Ref做什么

那么,考虑到这两者的不同,什么时候适合用Ref,而什么时候适合用state呢?需要考虑这些问题:

  • 这个值在现在或者将来,是否会用于渲染?
  • 个值在现在或者将来,是否会传递给其他组件?

如果这两个问题的答案都是否,那就可以使用Ref了。

比如说,我们可以使用Ref来保存组件的一些“dev”信息。我们也许想统计一个组件被渲染了多少次:

useEffect(() => {
    ref.current = ref.current + 1;
    
    console.log('Render number', ref.current);
});

又或者,你想访问一个状态的前值:

const usePrevious = (value) => {
    const ref = useRef();
    
    useEffect(() =>: {
        // this will be changed after the value is returned
        ref.current = value;
    }, [value])
    
    return ref.current;
}

之后,在useEffect中使用这个钩子:

useEffect(() => {
    if (previousValue.length > value.length) {
        console.log('Text was deleted');
    } else {
        console.log('Text was added');
    }
}, [previousValue, value])

代码示例: advanced-react.com/examples/09…

当然,最广泛的用法,还是用Ref来访问DOM。

使用 Ref 去访问DOM元素

我们只要用useRef创建一个Ref,再把这个Ref传递给组件的ref属性即可。

const Component = () => {
    const ref = useRef(null);
    
    // assing ref to an input element
    return <input ref={ref} />;
}

当这个input组件被渲染后,我们可以不使用DOM的原生API就访问input的DOM元素了:

const Component = () => {
    const ref = useRef(null);
    
    useEffect(() => {
        // this will be a reference to input DOM element!
        // excatly the same as if I die getElementById for it
        console.log(ref.current);
    })
    
    // assing ref to an input element
    return <input ref={ref} />;
}

有一个需要记住的点是:ref只会在元素被重新渲染后被赋值,或者是DOM元素被创建时赋值。我们需要把一些东西赋值给Ref,对吧?这意味着,ref.current并不是马上就有值的,这样的代码是无法运行的:

const Component = () => {
    const ref = useRef(null);
    
    // trying to access ref value before it was acttually assigned
    // input will never be rendered here
    if (!ref.current) return null
    
    return <input ref={ref} />;
}

我们只应该在useEffect或者回调函数里,读取或者写入ref.current

最后,回到我们最初关于一个精美注册表单的设想。如果我要把它实现为一个大型组件,我可以像这样做(不过后面应该还有具体的实现方式相关内容没展示出来呀,你可以补充完整以便我更深入地帮你分析或者探讨相关做法哦)。

const Form = () => {
    const [name, setName] = useState('');
    const inputRef = useRef(null);
    
    const onSubmitClick = () => {
        if (!name) {
            // focus the input field if someone tries to submit the empty name
            ref.current.focus();
        } else {
            // submit the data here!
        }
    };
    return (
        <>
            ...
            <input
                onChange={(e) => setName(e.target.value)}
                ref={ref}
            />
            <button onClick={onSubmitClick}>
                Submit the form!
            </button>
        </>
    );
};

把输入的值存在状态里,为表单控件创造refs。之后每次点击submit时,就可以检查是否每个值都填写了。如果有一些值为空,就可以展示相关提示了。

代码示例: advanced-react.com/examples/09…