简体   繁体   中英

How to access state when component unmount with React Hooks?

With regular React it's possible to have something like this:

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

}

In React Hooks, one could write this:

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

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

    return '...';
}

What's returned from useEffect will be executed only once before the component unmount, however the state (as in the code above) would be stale.

A solution would be to pass noteId as a dependency, but then the effect would run on every render, not just once. Or to use a reference, but this is very hard to maintain.

So is there any recommended pattern to implement this using React Hook?

With regular React, it's possible to access the state from anywhere in the component, but with hooks it seems there are only convoluted ways, each with serious drawbacks, or maybe I'm just missing something.

Any suggestion?

useRef() to the rescue.

Since the ref is mutable and exists for the lifetime of the component, we can use it to store the current value whenever it is updated and still access that value in the cleanup function of our useEffect via the ref's value .current property.

So there will be an additional useEffect() to keep the ref's value updated whenever the state changes.

Sample snippet

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

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

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

Thanks to the author of https://www.timveletta.com/blog/2020-07-14-accessing-react-state-in-your-component-cleanup-with-hooks/ . Please refer this link for deep diving.

useState() is a specialized form of useReducer() , so you can substitute a full reducer to get the current state and get around the closure problem.

NoteEditor

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;

App

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

I wanted to chime in with an answer for this in case someone else runs into this. If you need more than one value in your useEffect unmount function, it's important to make sure that the correct dependencies are being used. So the accepted answer works fine because it's just one dependency, but start including more dependencies, and it gets complicated. The amount of useRef's you need get out of hand. So instead, what you can do is a useRef that is the unmount function itself, and call that when you unmount the component:

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'
      />
    </>
  );
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM