Search code examples
javascriptreactjsstatereact-hooksuse-effect

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?


Solution

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