React:useState还是useRef?

147

我正在阅读有关React useState()useRef()的内容,位于“Hooks FAQ”,有些用例似乎可以同时使用useRef和useState解决,我不确定哪种方式才是正确的。

从“关于useRef()的FAQ”中得知:

“useRef() Hook不仅适用于DOM refs。‘ref’对象是一个通用容器,其current属性是可变的并且可以保存任何值,类似于类上的实例属性。”

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

  // ...
}

这两个示例将产生相同的结果,但哪一个更好 - 为什么呢?

12个回答

265

两者之间的主要区别是:

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

它们的共同点是,useStateuseRef 都可以在重新渲染后记住它们的数据。因此,如果您的变量是决定视图层渲染的内容,请使用 useState。否则,请使用 useRef

我建议阅读这篇文章


123
另一个很大的区别是,设置状态(state)是异步的,而设置引用(ref)是同步的。 - Lucas Willems
1
在实践中引入一个单独的(令人困惑的)钩子真的会有如此大的差异吗?React 在许多方面都不利于最佳性能(注册进出等),但我们应该关心避免一次重新渲染吗?为什么? - Robo Robok
2
当您在本机HTML输入元素中使用useRef时,您的组件是不受控制的输入,而使用useState则是受控制的。 - Sachin Yadav

14

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

我们可以借助useRefuseCallback依赖数组中删除currentPage的依赖关系,以便使我们的组件免于陷入无限循环。


8

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

  1. 引用的值在组件重新渲染之间保留不变

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

每次点击按钮,它都会显示 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>;
}

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


5

通常情况下,在需要重新渲染页面来更新状态值的情况下,我们使用UseState

当你想让信息在组件的整个生命周期中保持不变时,可以选择使用UseRef,因为它不仅用于重新渲染。


4
  • 计数器应用程序,以查看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> 这个值不会改变,因为组件没有重新渲染。如果您检查控制台 console.log(count.current),您会发现该值实际上在增加,但由于组件没有重新渲染,UI 不会得到更新。

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

  • 在输入时防止不必要的重新渲染 input

重新渲染是一项昂贵的操作。在某些情况下,您不希望保持应用程序的重新渲染。例如,当您将输入值存储在状态中以创建受控组件时。在这种情况下,对于每个按键,您都会重新渲染应用程序。如果您使用 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 内的无限循环

为了创建简单的翻转动画,我们需要两个状态值。一个是布尔值来在间隔中翻转或不翻转,另一个是在离开组件时清除订阅:

  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,我们将其传递给 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,每次更改状态值时都会重新渲染。

  • 假设您需要为组件使用id。如果使用useState创建它,则会在每次重新渲染时更改。

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

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

const id = useRef(uuid.v4());

1
如果您存储间隔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]);

  // ...
}

0

区别在于useState返回当前状态并具有更新状态的更新程序函数。而useRef返回一个对象,不会导致组件重新渲染,并且用于引用DOM元素。

因此,

如果您想在组件中拥有状态,当其更改时触发重新渲染视图,请使用useState或useReducer。如果您不希望状态触发渲染,请使用useRef。

看这个例子,

import { useEffect, useRef } from "react";
import { Form } from "./FormStyle";

const ExampleDemoUseRef = () => {
  const emailRef = useRef("");
  const passwordRef = useRef("");

  useEffect(() => {
    emailRef.current.focus();
  }, []);

  useEffect(() => {
    console.log("render everytime.");
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    const email = emailRef.current.value;
    const password = passwordRef.current.value;
    console.log({ email, password });
  };
  return (
    <div>
      <h1>useRef</h1>
      <Form onSubmit={handleSubmit}>
        <label htmlFor="email">Email: </label>
        <input type="email" name="email" ref={emailRef} />

        <label htmlFor="password">Password: </label>
        <input type="password" name="password" ref={passwordRef} />
        <button>Submit</button>
      </Form>
    </div>
  );
};

export default ExampleDemoUseRef;

这个useState的例子,

import { useEffect, useState, useRef } from "react";
import { Form } from "./FormStyle";

const ExampleDemoUseState = () => {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const emailRef = useRef("");

  useEffect(() => {
    console.log("render everytime.");
  });

  useEffect(() => {
    emailRef.current.focus();
  }, []);

  const onChange = (e) => {
    const { type, value } = e.target;

    switch (type) {
      case "email":
        setEmail(value);
        break;
      case "password":
        setPassword(value);
        break;
      default:
        break;
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    console.log({ email, password });
  };

  return (
    <div>
      <h1>useState</h1>
      <Form onSubmit={handleSubmit}>
        <label htmlFor="email">Email: </label>
        <input type="email" name="email" onChange={onChange} ref={emailRef} />

        <label htmlFor="password">Password: </label>
        <input type="password" name="password" onChange={onChange} />
        <button>Submit</button>
      </Form>
    </div>
  );
};

export default ExampleDemoUseState;

基本上,UseRef 是 useState 的替代方案,如果你不想更新 DOM 元素并想获取一个值(在组件中拥有状态),可以使用它。


0

你也可以使用useRef来引用一个dom元素(默认HTML属性)

例如:将一个按钮分配到焦点输入字段上。

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


0

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


0

这主要取决于你使用计时器的目的,因为你没有展示组件渲染的内容,所以不太清楚。

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

  • 如果有其他事情必须发生,应该在每个计时器滴答时在UI上进行可视化更改,你可以使用useState,并将计时器变量放入useEffect钩子的依赖数组中(在其中执行所需的UI更新),或者根据计时器值在render方法(组件返回值)中执行逻辑。 setState调用将导致重新渲染,然后调用您的useEffect钩子(取决于依赖项数组)。 使用ref,不会发生任何更新,也不会调用任何useEffect。

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

仅在真正必要的情况下(即在流程或性能问题的情况下)才应使用 refs 来处理本地状态,因为它不遵循“React 的方式”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接