最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to persist a variable between renders in React Hooks? - Stack Overflow

programmeradmin0浏览0评论

So, I've got a variable dog that I want to persist between re-renders.

const { useState, useEffect, useRef } = React;

class Animal {
  constructor(name) {
    this.name = name;
  }

  greet() {
    alert(`Hello I'm ${this.name}!`);
  }
}

const Dog = () => {
  let dog;
  useEffect(() => {
    dog = new Animal("Rusty", 5);
  }, []);
  return <button onClick={() => dog.greet()}>Greet</button>;
};

const App = () => {
  const [num, setNum] = useState(1);

  return (
    <main>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>Add 1</button>
      <br />
      <Dog />
    </main>
  );
};

ReactDOM.render(<App />, document.querySelector("#react-container"));
<div id="react-container"></div>

<script src="@17/umd/react.development.js"></script>
<script src="@17/umd/react-dom.development.js"></script>

So, I've got a variable dog that I want to persist between re-renders.

const { useState, useEffect, useRef } = React;

class Animal {
  constructor(name) {
    this.name = name;
  }

  greet() {
    alert(`Hello I'm ${this.name}!`);
  }
}

const Dog = () => {
  let dog;
  useEffect(() => {
    dog = new Animal("Rusty", 5);
  }, []);
  return <button onClick={() => dog.greet()}>Greet</button>;
};

const App = () => {
  const [num, setNum] = useState(1);

  return (
    <main>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>Add 1</button>
      <br />
      <Dog />
    </main>
  );
};

ReactDOM.render(<App />, document.querySelector("#react-container"));
<div id="react-container"></div>

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

In the snippet above there's the dog variable that I want to persist between re-renders. On the first render Dog component works fine and the alert is triggered successfully on clicking the Greet button.

But once there's a re-render which can be forced by clicking the Add 1 button, the dog variable is reset and now there's no greet method on it, so clicking Greet button throws an error.

I fixed this by using the useRef hook, just wanted to know if there's some better alternative or useRef is the best practice here.

So, I modified the Dog component to the following:

const Dog = () => {
  let dog = useRef(null);
  useEffect(() => {
    dog.current = new Animal("Rusty", 5);
  }, []);
  return <button onClick={() => dog.current.greet()}>Greet</button>;
};

const { useState, useEffect, useRef } = React;

class Animal {
  constructor(name) {
    this.name = name;
  }

  greet() {
    alert(`Hello I'm ${this.name}!`);
  }
}

const Dog = () => {
  let dog = useRef(null);
  useEffect(() => {
    dog.current = new Animal("Rusty", 5);
  }, []);
  return <button onClick={() => dog.current.greet()}>Greet</button>;
};

const App = () => {
  const [num, setNum] = useState(1);

  return (
    <main>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>Add 1</button>
      <br />
      <Dog />
    </main>
  );
};

ReactDOM.render(<App />, document.querySelector("#react-container"));
<div id="react-container"></div>

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

Share Improve this question asked Jul 16, 2021 at 17:05 anonanon 4
  • 1 useState(dog) ? – Evert Commented Jul 16, 2021 at 17:07
  • const [dog, setDog] = useState(); – Samathingamajig Commented Jul 16, 2021 at 17:12
  • 4 @Evert No, you want to store in the state variables that should trigger renders, but if you want to keep other variables around, useRef is the way to go. – hjrshng Commented Jul 16, 2021 at 17:12
  • 1 @SomShekharMukherjee In my opinion, yes. As the doc says also: It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes. – hjrshng Commented Jul 16, 2021 at 18:02
Add a comment  | 

2 Answers 2

Reset to default 16

Let's understand line by line what's happening with the first implementation of the Dog functional component.

1. const Dog = () => {
2.   let dog;
3.   useEffect(() => {
4.     dog = new Animal("Rusty", 5);
5.   }, []);
6.   return <button onClick={() => dog.greet()}>Greet</button>;
7. };
  • In line no. 2 we define a variable named dog, nice and simple.

  • From line no. 3 to 5, we call the useEffect hook and pass in a callback function to it. The callback has a closure over the variable dog that we created in line no. 1.
    Also, since we have passed [] as dependency array this means the callback will only be called once when the component is mounted.

  • Finally in line no. 6 we return a button whose onClick handler also has a closure over the same dog variable.

Dog component on mount

When the Dog component is called for the first time (on mount) following things happen:

  1. dog variable is created.
  2. useEffect hook is called and a callback is passed (note that the callback is just passed and not called).
  3. A button is returned.
  4. And finally the useEffect callback passed in step 2 is called and because it has a closure over the dog variable, the same dog variable that was created in step one get's updated to store an instance of the Animal class (i.e. new Animal("Rusty", 5)).

Greet button is clicked

Now, when the "Greet" button is clicked, it's onClick handler is called, which has closure over the same dog variable which got updated to "Rusty", therefore we get the expected alert on the screen.

Dog component on second render

When the Dog component is called again (by making state changes in the parent component) following things happen:

  1. A new dog variable is created.
  2. useEffect hook is called and a callback is passed, but note that this callback will never be called because of how the dependency array is set up.
  3. Finally a button is returned whose onClick handler now has a closure over the new dog variable.

Greet button is clicked again

Now when the "Greet" button is clicked again, the onClick handler is called, which now has a closure over the new dog variable and this time the variable is undefined, so we get an error.

So, the point to note here is that with functional component every time there's a re-render all the variables inside the function get re-created and closures over these variables also get updated.

Solution

The solution with useRef is good enough, useRef offers the following two things:

  1. It's value persists between re-renders.
  2. And it can be mutated without causing a re-render.

But in this case the solution can be made even better by moving the dog variable out of the component.

This solution works really well if you want to instantiate a library once and then call methods on that instance inside your component.

const { useState } = React;

class Animal {
  constructor(name) {
    this.name = name;
  }

  greet() {
    alert(`Hello I'm ${this.name}!`);
  }
}

const dog = new Animal("Rusty", 5);

const Dog = () => {
  return <button onClick={() => dog.greet()}>Greet</button>;
};

const App = () => {
  const [num, setNum] = useState(1);

  return (
    <main>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>Add 1</button>
      <br />
      <Dog />
    </main>
  );
};

ReactDOM.render(<App />, document.querySelector("#react-container"));
<div id="react-container"></div>

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

On a sidenote, useMemo should not be used for this, it is only meant for optimisations.

Update:

As it is needed to load a library, it is better to persist it in the React Context, so it can be used in other components too:

Context:

import { ReactNode, useState } from 'react';

const ApiRegister = ({ children }: { children: ReactNode }) => {
  const [api, setApi] = useState(null);

  const registerApi = api => setApi(api);

  return (
    <ApiContext.Provider
      value={{
        api,
        registerApi,
      }}
    >
      {children}
    </ApiContext.Provider>
  );
};

export default ApiRegister;

Saving API to context:

const ApiInitializer = () => {
  const dispatch = useDispatch();
  const { api, registerApi }: ContextType = useContext(ApiContext);

  useEffect(() => {
    const api = initializeApi();
    api.then(api => registerApi(api));
  }, [dispatch, registerApi]);


  return (
    <Component />
  );
};

export default ApiInitializer;

Old answer:

useMemo could be a good solution here, seems cleaner than refs:

const Dog = () => {
  const dog = useMemo(() => new Animal('Rusty', 5), []);

  return <button onClick={() => dog.greet()}>Greet</button>;
};
发布评论

评论列表(0)

  1. 暂无评论