你可能需要避免的5个react的ref错误用法

前言

react是一个优秀的框架,提供了我们很多的便利,但是在使用的过程中,我们也会遇到很多的问题,其中一个就是ref的使用,以下是我列出的5个使用ref的错误用法,并提供了正确的用法。

错误1: 当使用ref更好时,却使用state

一个常见的错误就是明明使用ref更合适管理状态的时候,但是却使用state来存储状态。例如存储定时器的间隔时间。

错误用法: 我们将定时器间隔时间存储在状态中从而触发了不必要的重新渲染。

tsx 复制代码
import { useState, useEffect } from 'react';

export const CustomTimer = () => {
  const [intervalId, setIntervalId] = useState();
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    setIntervalId(id);
    return () => clearInterval(id);
  }, []);

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  }

  return (
    <div>
      <p>{time.toLocaleString()}</p>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

正确用法:将定时器间隔存储在ref中,从而避免了不必要的重新渲染。

ref有个好处就是不会触发组件的重新渲染,从而避免了不必要的性能问题。

tsx 复制代码
import { useRef, useEffect } from'react';
 
export const CustomTimer = () => {
  const intervalIdRef = useRef();
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const id = setInterval(() => {
      setTime(new Date());
    }, 1000);
    intervalIdRef.current = id;
    return () => clearInterval(id);
  }, []);
  const stopTimer = () => {
    intervalIdRef.current && clearInterval(intervalIdRef.current);
  }
  return (
    <div>
      <p>{time.toLocaleString()}</p>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

错误2: 在设置ref的值之前使用ref.current而不是ref

我们在使用ref传递值给某个函数或者子组件的时候,使用的是ref.current而不是ref本身,直接使用ref本身的情况下,ref本身就是一个变化的对象,我们可以在组件渲染时使用ref.current来获取当前的值,但是在设置ref的值之前,ref.current的值是undefined,这就会导致我们的代码出现错误。

错误用法: 下面的代码无法运行,因为ref.current最初为空, 因此,当代码运行时,element为空。

tsx 复制代码
import { useRef } from'react';

const useHovered = (element) => {
  const [hovered, setHovered] = useState(false);

  useEffect(() => {
    if(element === null) return;
      element.addEventListener('mouseenter', () => setHovered(true));
      element.addEventListener('mouseleave', () => setHovered(false));
    return () => {
        element.removeEventListener('mouseenter', () => setHovered(true));
        element.removeEventListener('mouseleave', () => setHovered(false));
    };
  }, [element]);

  return hovered;
}
export const CustomHoverDivElement = () => {
  const ref = useRef();
  const isHoverd = useHovered(ref.current);
  return (
    <div ref={ref}>
       Hoverd:{`${isHoverd}`}
    </div>
  );
}

正确用法: 我们需要在设置ref的值之前使用ref.current来获取当前的值。

tsx 复制代码
import { useRef } from'react';
const useHovered = (ref) => {
  const [hovered, setHovered] = useState(false);
  useEffect(() => {
    if(ref.current === null) return;
      ref.current.addEventListener('mouseenter', () => setHovered(true));
      ref.current.addEventListener('mouseleave', () => setHovered(false));
    return () => {
        ref.current.removeEventListener('mouseenter', () => setHovered(true));
        ref.current.removeEventListener('mouseleave', () => setHovered(false));
    };
  }, [ref]);
  return hovered;
}
export const CustomHoverDivElement = () => {
  const ref = useRef();
  const isHoverd = useHovered(ref);
  return (
    <div ref={ref}>
      Hoverd:{`${isHoverd}`}
    </div>
  );
}

错误3: 忘记使用fowardRef

在初学react时,我们可能都犯过这个错误,直接给组件传递ref参数。事实上,React 不允许你将 ref 传递给函数组件,除非它被forwardRef包装起来。解决办法是什么?只需将接收 ref 的组件包装在 forwardRef 中,或为 ref prop 使用另一个名称即可。

错误用法: 下面的代码无法运行,因为我们没有使用forwardRef来包装组件。

tsx 复制代码
import { useRef } from'react';
const CustomInput = ({ ref,...rest }) => {
    const [value, setValue] = useState('');
  useEffect(() => {
    if(ref.current === null) return;
    ref.current.focus();
  }, [ref]);
  return (
    <input ref={ref} {...rest} value={value} onChange={e => setValue(e.target.value)} />
  );
}
export const CustomInputElement = () => {
  const ref = useRef();
  return (
    <CustomInput ref={ref} />
  );
}

正确用法: 我们需要使用forwardRef来包装组件。

tsx 复制代码
import { useRef, forwardRef } from'react';
const CustomInput = forwardRef((props, ref) => {
  const [value, setValue] = useState('');
  useEffect(() => {
    if(ref.current === null) return;
    ref.current.focus();
  }, [ref]);
  return (
    <input ref={ref} {...props} value={value} onChange={e => setValue(e.target.value)} />
  );
})
export const CustomInputElement = () => {
  const ref = useRef();
  return (
    <CustomInput ref={ref} />
  );
}

错误4: 调用函数来初始化ref的值

当你调用函数来设置 ref 的初始值时,该函数将在每次渲染时被调用,如果该函数开销很大,这将不必要地影响你的应用性能。解决方案是什么?缓存该函数或在渲染期间初始化 ref(在检查值尚未设置之后)。

错误用法: 下面的代码很浪费性能,因为我们在每次渲染时都调用了函数来设置 ref 的初始值。

tsx 复制代码
import { useState, useRef, useEffect } from "react";

const useOnBeforeUnload = (callback) => {
  useEffect(() => {
    window.addEventListener("beforeunload", callback);
    return () => window.removeEventListener("beforeunload", callback);
  }, [callback]);
}

export const App = () => {
  const ref = useRef(window.localStorage.getItem("cache-date"));
  const [inputValue, setInputValue] = useState("");

  useOnBeforeUnload(() => {
    const date = new Date().toUTCString();
    console.log("Date", date);
    window.localStorage.setItem("cache-date", date);
  });

  return (
    <>
      <div>
        缓存的时间: <strong>{ref.current}</strong>
      </div>
      用户名:{" "}
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </>
  );
}

正确用法: 我们需要缓存该函数或在渲染期间初始化 ref(在检查值尚未设置之后)。

tsx 复制代码
import { useState, useRef, useEffect } from "react";
const useOnBeforeUnload = (callback) => {
  useEffect(() => {
    window.addEventListener("beforeunload", callback);
    return () => window.removeEventListener("beforeunload", callback);
  }, [callback]);
}
export const App = () => {
  const ref = useRef(null);
  if (ref.current === null) {
    ref.current = window.localStorage.getItem("cache-date");
  }
  const [inputValue, setInputValue] = useState("");
  useOnBeforeUnload(() => {
    const date = new Date().toUTCString();
    console.log("Date", date);
    window.localStorage.setItem("cache-date", date);
  });

  return (
    <>
      <div>
        缓存的时间: <strong>{ref.current}</strong>
      </div>
      用户名:{" "}
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
    </>
  );
}

错误5: 使用每次渲染都会改变的ref回调函数

ref 回调函数可以很好的管理你的代码,但是,请注意,每当更改时,React 都会调用 ref 回调。这意味着当组件重新渲染时,前一个函数将使用 null 作为参数调用,而下一个函数将使用 DOM 节点调用。这可能会导致 UI 中出现一些不必要的闪烁。解决方案?确保缓存(使用useCallback) ref 回调函数。

错误用法: 下面的代码无法正常工作,因为每当inputValue或currentTime发生变化时,ref 回调函数就会再次运行,并且输入将再次成为焦点。

tsx 复制代码
import { useEffect, useState } from "react";

const useCurrentTime = () => {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  });
  return time.toString();
}

export const App = () => {
  const ref = (node) => {
    node?.focus();
  };
  const [nameValue, setNameValue] = useState("");
  const currentTime = useCurrentTime();

  return (
    <>
      <h2>当前时间: {currentTime}</h2>
      <label htmlFor="name">用户名: </label>
      <input
        id="name"
        ref={ref}
        value={nameValue}
        onChange={(e) => setNameValue(e.target.value)}
      />
    </>
  );
}

正确用法: 我们需要确保缓存(使用useCallback) ref 回调函数。

tsx 复制代码
import { useEffect, useState, useCallback } from "react";
const useCurrentTime = () => {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  });
  return time.toString();
}
export const App = () => {
  const ref = useCallback((node) => {
    node?.focus();
  }, []);
  const [nameValue, setNameValue] = useState("");
  const currentTime = useCurrentTime();
  return (
    <>
      <h2>当前时间: {currentTime}</h2>
      <label htmlFor="name">用户名: </label>
      <input
        id="name"
        ref={ref}
        value={nameValue}
        onChange={(e) => setNameValue(e.target.value)}
      />
    </>
  );
}

最后,感谢阅读这篇文章,希望对你有所帮助,谢谢!如果觉得有用,可以动动小手点赞收藏!

相关推荐
CQU_JIAKE几秒前
12.12【java exp4】react table全局搜索tailwindcss 布局 (Layout) css美化 3. (rowId: number
前端·javascript·react.js
racerun1 小时前
Vue vuex.store mapState
前端·javascript·vue.js
yep吖1 小时前
Datawhale-AI冬令营二期
开发语言·javascript·ecmascript
胡西风_foxww1 小时前
【ES6复习笔记】箭头函数(5)
javascript·笔记·es6·函数·箭头·箭头函数
残花月伴2 小时前
axios
javascript
Ares码农人生2 小时前
React 前端框架简介
前端·react.js·前端框架
GISer_Jing2 小时前
前端经典面试合集(二)——Vue/React/Node/工程化工具/计算机网络
前端·vue.js·react.js·node.js
van叶~3 小时前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
泯泷3 小时前
JS代码混淆器:JavaScript obfuscator 让你的代码看起来让人痛苦
开发语言·javascript·ecmascript
高兴蛋炒饭10 小时前
RouYi-Vue框架,环境搭建以及使用
前端·javascript·vue.js