反应:useState 还是 useRef?

React: useState or useRef?

提问人:Haim763 提问时间:6/5/2019 最后编辑:YilmazHaim763 更新时间:5/29/2023 访问量:65633

问:

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

关于 useRef() 的 “Hooks FAQ” 中:

“useRef() Hook 不仅适用于 DOM 引用。“ref”对象是一个泛型容器,其当前属性是可变的,可以保存任何值,类似于类上的实例属性。

使用 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);
    };
  });

  // ...
}

两个例子都会有相同的结果,但哪一个更好 - 为什么?

reactjs 反应钩子

评论


答:

265赞 Easwar 6/5/2019 #1

两者之间的主要区别在于:

useState导致重新渲染,则不然。useRef

它们之间的共同点是,两者都可以在重新渲染后记住它们的数据。因此,如果您的变量决定了视图图层渲染,请使用 。否则使用useStateuseRefuseStateuseRef

我建议阅读这篇文章

评论

123赞 Lucas Willems 12/9/2019
另一个很大的区别是,设置状态是异步的,而设置引用是同步的。
1赞 Robo Robok 12/21/2020
在实践中引入一个单独的(令人困惑的)钩子真的有如此大的区别吗?React 做了很多不利于最佳性能的事情(注册内容和注册输出),但我们应该关心避免一次重新渲染吗?怎么会这样?
2赞 Sachin Yadav 12/16/2021
当您将 useRef 与本机 HTML 输入元素一起使用时,您的组件是不受控制的输入,而使用 useState 则是受控的
5赞 Ghulam Meeran 11/16/2020 #2

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

当您希望您的信息在组件的生命周期内保留时,您将使用 UseRef,因为它不适用于重新渲染。

1赞 user3654410 12/29/2020 #3

如果存储间隔 ID,则唯一能做的就是结束间隔。更好的是存储状态,以便您可以在需要时停止/启动计时器。timerActive

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]);

  // ...
}
14赞 glinda93 7/6/2021 #4

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正在使用状态,但需要在成功响应后进行更新。这是不可避免的过程,但它很容易导致无限循环,也就是 React 中的无限循环。例如,如果要在加载组件时获取行,则需要执行如下操作:currentPagecurrentPageMaximum update depth exceeded error

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

这是有缺陷的,因为我们使用状态并在同一函数中更新它。

我们想跟踪,但不想触发或通过它的变化。currentPageuseCallbackuseEffect

我们可以通过以下方法轻松解决这个问题: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]);

我们可以借助 从 deps 数组中删除依赖项,因此我们的组件从无限循环中保存下来。currentPageuseCallbackuseRef

0赞 Raghavan Vidhyasagar 9/10/2021 #5

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

例如:分配一个按钮以专注于输入字段。

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

0赞 Ruben Lemiengre 11/27/2021 #6

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

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

  • 如果必须发生其他事情,这应该在计时器的每个刻度直观地更改 UI,请使用 useState 并将 timer 变量放在 useEffect 钩子的依赖项数组中(您可以在其中执行 UI 更新所需的任何操作),或者根据计时器值在 render 方法(组件返回值)中执行逻辑。 SetState 调用将导致重新渲染,然后调用 useEffect 钩子(取决于依赖项数组)。 使用 ref 时,不会发生任何更新,也不会调用 useEffect。

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

只有在真正必要时(即在出现流程或性能问题的情况下),才应该对本地状态使用 refs,因为它不遵循“React 方式”。

4赞 Yilmaz 3/8/2022 #7
  • 要查看的计数器应用不会重新呈现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>
  );
};

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

如果使用 设置状态,请单击该按钮将重新呈现组件,以便更新 UI。useState

  • 防止在键入 时进行不必要的重新渲染。input

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

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返回一个 ID,当我们离开组件时,我们将其传递给以结束订阅。 为 null 或此 id。如果我们不在这里使用,每次我们从 null 切换到 id 或从 id 切换到 null 时,这个组件都会重新渲染,这将创建一个无限循环。clearIntervalflipInterval.currentref

  • 如果不需要更新 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

  • 假设您需要为组件使用 ID。如果你用它创建它,它会随着每次重新渲染而改变。useState

    常量 [id,setId]=useState(uuid.v4())

如果您希望 id 在每次重新渲染时都不更改

const id = useRef(uuid.v4());
0赞 CRN 3/8/2022 #8

useRef() 只更新值,而不是重新渲染你的 UI,如果你想重新渲染 UI,那么你必须使用 useState() 而不是 useRe。如果需要更正,请告诉我。

8赞 Bidisha Das 5/23/2022 #9

useState 和 useRef 之间的主要区别是 -

  1. 引用的值在组件重新渲染之间保持不变(保持不变),

  2. 更新引用不会触发组件重新渲染。 但是,更新状态 c会重新渲染组件useRef

  3. 引用更新是同步的,更新的引用值立即可用,但状态更新是异步的 - 该值在重新呈现后更新。

要查看使用代码:

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>;
}

每次单击该按钮时,它都会显示我已渲染!

但是,随着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>;
}

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

0赞 Archimedes Trajano 12/20/2022 #10

如前所述,在许多不同的地方,更新会触发组件的渲染,而更新则不会。useStateuseRef

在大多数情况下,有一些指导原则会有所帮助:

useState

  • 与 / 一起使用的任何内容都应该有一个状态,该状态会随着您设置的值而更新。inputTextInput
  • 当您需要触发器来重新计算 中的值或触发效果时useMemouseEffect
  • 当您需要呈现所使用的数据时,该呈现仅在对事件处理程序或其他事件处理程序执行操作后才可用。例如 需要提供的数据。asyncuseEffectFlatList

useRef

  • 使用它们来存储用户不可见的数据,例如事件订阅者。
  • 对于上下文或自定义钩子,请使用它来传递由 / 更新或由 / 触发的道具。我倾向于犯的错误是将类似的东西放置为状态,然后当我更新时,当该状态实际上是链的最终结果时,它会触发整个重新渲染。useMemouseEffectuseStateuseReducerauthState
  • 当您需要通过ref
0赞 Adil Tuladhar 5/26/2023 #11

简单地说,如果你只需要读取一个值而从不更新该值,那么请使用 Refs