反应:useState 还是 useRef?

React: useState or useRef?

我正在“Hooks FAQ”阅读有关 React useState()useRef() 的文章,我对一些似乎有 useRef 和 useState 解决方案的用例感到困惑同时,我不确定哪种方式是正确的。

来自“Hooks FAQ”about useRef():

"The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class."

使用 useRef():

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

使用useState():

function Timer() {
  const [intervalId, setIntervalId] = useState(null);

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    setIntervalId(id);
    return () => {
      clearInterval(intervalId);
    };
  });

  // ...
}

两个示例的结果相同,但哪个更好 - 为什么?

两者的主要区别是:

useState 导致 re-render,useRef 不会。

他们的共同点是,useStateuseRef都可以在re-render秒后记住他们的数据。因此,如果您的变量决定视图层渲染,请使用 useState。否则使用 useRef

我建议阅读这篇 article

基本上,我们在这些情况下使用UseState,其中state的值应该通过重新渲染来更新。

如果您希望您的信息在组件的生命周期内持续存在,您将使用 UseRef,因为它不适用于重新渲染。

如果你存储间隔id,你唯一能做的就是结束间隔。更好的方法是存储状态 timerActive,这样您就可以在需要时 stop/start 计时器。

function Timer() {
  const [timerActive, setTimerActive] = useState(true);

  useEffect(() => {
    if (!timerActive) return;
    const id = setInterval(() => {
      // ...
    });
    return () => {
      clearInterval(intervalId);
    };
  }, [timerActive]);

  // ...
}

如果您希望回调在每次渲染时都发生变化,您可以使用 ref 在每次渲染时更新内部回调。

function Timer() {
  const [timerActive, setTimerActive] = useState(true);
  const callbackRef = useRef();

  useEffect(() => {
    callbackRef.current = () => {
      // Will always be up to date
    };
  });

  useEffect(() => {
    if (!timerActive) return;
    const id = setInterval(() => {
      callbackRef.current()
    });
    return () => {
      clearInterval(intervalId);
    };
  }, [timerActive]);

  // ...
}

useRef 在您想要跟踪值更改但不想触发重新渲染或 useEffect 时很有用。

大多数用例是当您有一个依赖于值的函数,但该值需要由函数结果本身更新时。

例如,假设您要对某些 ​​API 结果进行分页:

const [filter, setFilter] = useState({});
const [rows, setRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1);

const fetchData = useCallback(async () => {
  const nextPage = currentPage + 1;
  const response = await fetchApi({...filter, page: nextPage});
  setRows(response.data);
  if (response.data.length) {
    setCurrentPage(nextPage);
  }
}, [filter, currentPage]);

fetchData正在使用currentPage状态,但响应成功后需要更新currentPage。这是不可避免的过程,但在 React 中很容易导致死循环,也就是 Maximum update depth exceeded error。例如,如果你想在加载组件时获取行,你想做这样的事情:

useEffect(() => {
  fetchData();
}, [fetchData]);

这是错误的,因为我们使用状态并在同一个函数中更新它。

我们想跟踪 currentPage 但不想通过它的变化触发 useCallbackuseEffect

我们可以用useRef轻松解决这个问题:

const currentPageRef = useRef(0);

const fetchData = useCallback(async () => {
  const nextPage = currentPageRef.current + 1;
  const response = await fetchApi({...filter, page: nextPage});
  setRows(response.data);
  if (response.data.length) {
     currentPageRef.current = nextPage;
  }
}, [filter]);

我们可以在 useRef 的帮助下从 useCallback deps 数组中删除 currentPage 依赖项,这样我们的组件就不会无限循环了。

您还可以使用 useRef 来引用 dom 元素(默认 HTML 属性)

例如:分配一个按钮以聚焦在输入字段上。

useState 仅更新值并重新呈现组件。

这实际上主要取决于您使用计时器的目的,这并不清楚,因为您没有显示组件呈现的内容。

  • 如果你想在你的组件渲染中显示你的计时器的值,你需要使用useState。否则,您的 ref 的更改值将不会导致重新渲染,并且计时器不会在屏幕上更新。

  • 如果必须发生其他事情,应该 计时器的每个滴答声 视觉上更改 UI ],您使用 useState 并将计时器变量放在 useEffect 挂钩的依赖项数组中(您可以在其中执行 UI 更新所需的任何操作),或者在渲染方法中执行您的逻辑(组件 return 值)基于定时器值。 SetState 调用将导致重新渲染,然后调用您的 useEffect 挂钩(取决于依赖项数组)。 使用 ref,不会发生更新,也不会调用 useEffect。

  • 如果您只想在内部使用定时器,您可以改用 useRef。每当必须发生导致重新渲染的事情时(即经过一定时间后),您就可以在 setInterval 回调中使用 setState 调用另一个状态变量。这将导致组件重新渲染。

仅在真正需要时(即在出现流程或性能问题时)才应使用本地状态的 refs,因为它不遵循“the React way”。

  • 计数器 App 看到 useRef 不重新渲染

如果您使用 useRef 创建一个简单的计数器应用程序来存储状态:

import { useRef } from "react";

const App = () => {
  const count = useRef(0);

  return (
    <div>
      <h2>count: {count.current}</h2>
      <button
        onClick={() => {
          count.current = count.current + 1;
          console.log(count.current);
        }}
      >
        increase count
      </button>
    </div>
  );
};

如果您单击该按钮, <h2>count: {count.current}</h2> 该值不会改变,因为组件不是 RE-RENDERING。如果您检查控制台 console.log(count.current),您会看到该值实际上在增加,但由于组件没有重新渲染,UI 没有得到更新。

如果您使用 useState 设置状态,则单击按钮会重新呈现组件,因此 UI 会得到更新。

  • 防止输入 input 时出现不必要的 re-renderings。

重新渲染是一项昂贵的操作。在某些情况下,您不想继续重新呈现应用程序。例如,当您将输入值存储在状态中以创建受控组件时。在这种情况下,对于每次击键,您都会重新呈现应用程序。如果您使用 ref 获取对 DOM 元素的引用,使用 useState 您将仅重新渲染组件一次:

import { useState, useRef } from "react";
const App = () => {
  const [value, setValue] = useState("");
  const valueRef = useRef();
 
  const handleClick = () => {
    console.log(valueRef);
    setValue(valueRef.current.value);
  };
  return (
    <div>
      <h4>Input Value: {value}</h4>
      <input ref={valueRef} />
      <button onClick={handleClick}>click</button>
    </div>
  );
};
  • 防止内部无限循环useEffect

要创建一个简单的翻转动画,我们需要 2 个状态值。一个是一个布尔值是否在一个时间间隔内翻转,另一个是当我们离开组件时清除订阅:

const [isFlipping, setIsFlipping] = useState(false);
  
  let flipInterval = useRef<ReturnType<typeof setInterval>>();
  useEffect(() => {
    startAnimation();
    return () => flipInterval.current && clearInterval(flipInterval.current);
  }, []);

  const startAnimation = () => {
    flipInterval.current = setInterval(() => {
      setIsFlipping((prevFlipping) => !prevFlipping);
    }, 10000);
  };

setInterval returns 一个 id,我们将它传递给 clearInterval 以在我们离开组件时结束订阅。 flipInterval.current 为 null 或此 ID。如果我们在这里不使用 ref,每次我们从 null 切换到 id 或从 id 切换到 null 时,这个组件都会重新渲染,这将创建一个无限循环。

  • 如果不需要更新UI,使用useRef存储状态变量。

假设在 React Native 应用程序中,我们为某些对 UI 没有影响的操作设置了声音。对于一个状态变量,它可能不会节省太多性能,但如果你玩游戏,你需要根据游戏状态设置不同的声音。

const popSoundRef = useRef<Audio.Sound | null>(null);
const pop2SoundRef = useRef<Audio.Sound | null>(null);
const winSoundRef = useRef<Audio.Sound | null>(null);
const lossSoundRef = useRef<Audio.Sound | null>(null);
const drawSoundRef = useRef<Audio.Sound | null>(null);

如果我使用 useState,我会在每次更改状态值时继续重新渲染。

useRef() 只更新值而不是 re-render 你的 UI 如果你想 re-render UI 那么你必须使用 useState() 而不是 useRe。让我知道是否需要更正。

useState 和 useRef 的主要区别是 -

  1. 引用的值在组件re-rendering,

    之间persisted(保持不变)
  2. 使用 useRef 更新引用不会触发 组件 re-rendering。 但是,更新状态 c 会导致组件 re-rendering

  3. 引用更新是同步,更新的引用值是立即可用的,但是状态更新是异步的 - 值在 re-rendering.

    后更新

使用代码查看:

import { useState } from 'react';
function LogButtonClicks() {
  const [count, setCount] = useState(0);
  
  const handle = () => {
    const updatedCount = count + 1;
    console.log(`Clicked ${updatedCount} times`);
    setCount(updatedCount);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

每次点击按钮都会显示I rendered!

但是,useRef

import { useRef } from 'react';
function LogButtonClicks() {
  const countRef = useRef(0);
  
  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

我被渲染 将被控制台记录一次