使用 React Hooks 卸载组件时如何访问状态?

Posted

技术标签:

【中文标题】使用 React Hooks 卸载组件时如何访问状态?【英文标题】:How to access state when component unmount with React Hooks? 【发布时间】:2020-06-12 20:35:03 【问题描述】:

使用常规的 React 可以有这样的东西:

class NoteEditor extends React.PureComponent 

    constructor() 
        super();

        this.state = 
            noteId: 123,
        ;
    

    componentWillUnmount() 
        logger('This note has been closed: ' + this.state.noteId);
    

    // ... more code to load and save note


在 React Hooks 中,可以这样写:

function NoteEditor 
    const [noteId, setNoteId] = useState(123);

    useEffect(() => 
        return () => 
            logger('This note has been closed: ' + noteId); // bug!!
        
    , [])

    return '...';

useEffect 返回的内容只会在组件卸载之前执行一次,但是状态(如上面的代码中所示)将是陈旧的。

一种解决方案是将noteId 作为依赖项传递,但效果会在每个渲染上运行,而不仅仅是一次。或者使用引用,但这很难维护。

那么有没有推荐的模式来使用 React Hook 来实现呢?

使用常规的 React,可以从组件中的任何位置访问状态,但使用钩子似乎只有复杂的方式,每种方式都有严重的缺陷,或者我只是遗漏了一些东西。

有什么建议吗?

【问题讨论】:

“复杂的方式”:钩子往往比基于类的对应物要简单得多。 “严重的缺点”:我很想知道这些是什么。 “但这很难维护”:为什么 ref 很难维护?当nodeId改变时更新ref,用当前的useEffect读取? 这能回答你的问题吗? componentWillUnmount with React useEffect "当 nodeId 改变时更新 ref" - 这意味着每次 nodeId 改变时有两个变量而不是一个要更新。当然可以,但这并不简单。事实上,即使是 React Hooks 文档也建议不要使用 ref(尽管他们没有说明要使用什么)。 类组件有时确实更有意义。这可能是其中一种情况。或者依靠卸载的组件来跟踪是否已关闭可能不是最好的方法? "但是效果会在每次渲染上运行" - 应该是 "但是效果会在 noteId 更改时运行" 【参考方案1】:

useState()useReducer() 的一种特殊形式,因此您可以替换一个完整的reducer 来获取当前状态并解决闭包问题。

笔记编辑器

import React,  useEffect, useReducer  from "react";

function reducer(state, action) 
  switch (action.type) 
    case "set":
      return action.payload;
    case "unMount":
      console.log("This note has been closed: " + state); // This note has been closed: 201
      break;
    default:
      throw new Error();
  


function NoteEditor( initialNoteId ) 
  const [noteId, dispatch] = useReducer(reducer, initialNoteId);

  useEffect(function logBeforeUnMount() 
    return () => dispatch( type: "unMount" );
  , []);

  useEffect(function changeIdSideEffect() 
    setTimeout(() => 
      dispatch( type: "set", payload: noteId + 1 );
    , 1000);
  , []);

  return <div>noteId</div>;

export default NoteEditor;

应用程序

import React,  useState, useEffect  from "react";
import "./styles.css";
import NoteEditor from "./note-editor";

export default function App() 
  const [notes, setNotes] = useState([100, 200, 300]);

  useEffect(function removeNote() 
    setTimeout(() => 
      setNotes([100, 300]);
    , 2000);
  , []);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      notes.map(note => (
        <NoteEditor key=`Note$note` initialNoteId=note />
      ))
    </div>
  );

【讨论】:

这是技术上不让您访问使用效果清理功能中的状态。 useRef() 钩子的当前属性应该引用状态,然后您可以在清理中直接访问状态。 见github.com/facebook/react/issues/14285 ? dispatch() 不会在卸载时被调用(无论如何对我来说),因为预计减速器不会有副作用。【参考方案2】:

useRef() 进行救援。

由于 ref 是可变的并且在组件的生命周期中存在,我们可以在它更新时使用它来存储当前值,并且仍然可以在我们的 的清理函数中访问该值>useEffect 通过 ref 的 value .current 属性。

所以会有一个额外的 useEffect() 来保持 ref 的值在状态改变时更新。

示例 sn-p

const [value, setValue] = useState();
const valueRef = useRef();

useEffect(() => 
  valueRef.current = value;
, [value]);

useEffect(() => 
  return function cleanup() 
    console.log(valueRef.current);
  ;
, []);

感谢https://www.timveletta.com/blog/2020-07-14-accessing-react-state-in-your-component-cleanup-with-hooks/的作者。请参考此链接进行深度潜水。

【讨论】:

当然可以使用 Refs,但不是只有一个值,而是需要两个值保持同步。 救命稻草,谢谢!【参考方案3】:

我想回答这个问题,以防其他人遇到这个问题。如果您的 useEffect 卸载函数中需要多个值,请务必确保使用正确的依赖项。所以接受的答案很好,因为它只是一个依赖项,但开始包含更多依赖项,它变得复杂。您需要的 useRef 数量会失控。因此,您可以做的是一个 useRef,它是卸载函数本身,并在卸载组件时调用它:

import React,  useRef, useState, useContext, useCallback, useEffect  from 'react';
import  Heading, Input  from '../components';
import  AppContext  from '../../AppContext';

export const TitleSection: React.FC = ( thing ) => 
  const  updateThing  = useContext(AppContext);
  const [name, setName] = useState(thing.name);
  const timer = useRef(null);
  const onUnmount = useRef();

  const handleChangeName = useCallback((event) => 
    setName(event.target.value);

    timer.current !== null && clearTimeout(timer.current);
    timer.current = setTimeout(() => 
      updateThing(
        name: name || ''
      );
      timer.current = null;
    , 1000);
  , [name, updateThing]);

  useEffect(() => 
    onUnmount.current = () => 
      if (thing?.name !== name) 
        timer.current !== null && clearTimeout(timer.current);
        updateThing(
          name: name || '',
        );
        timer.current = null;
      
    ;
  , [thing?.name, name, updateThing]);

  useEffect(() => 
    return () => 
      onUnmount.current?.();
    ;
  , []);

  return (
    <>
      <Heading as="h1" fontSize="md" style= marginBottom: 5 >
        Name
      </Heading>
      <Input
        placeholder='Grab eggs from the store...'
        value=name
        onChange=handleChangeName
        variant='white'
      />
    </>
  );
;

【讨论】:

以上是关于使用 React Hooks 卸载组件时如何访问状态?的主要内容,如果未能解决你的问题,请参考以下文章

使用 hooks 时 React 组件名称必须以大写字母开头

为何要使用React Hooks?

使用 React-Hooks 单击按钮后如何显示不同的组件?

如何使用 React Hooks 实现复杂组件的状态管理

使用 React Hooks,我如何制作一个可重用的组件和父组件相互利用?

如何在 React hooks 和 redux 中只获取一次且不再获取数据