Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Avoid useState hook to run on initialisation

I have a simple button, which subscribes and unsubscribes onPress()

<Button
      onPress={() => setRunning(running => !running)}
      title={running ? 'Stop' : 'Start'}
/>

But when I initialise my state, it runs the useState function which is fatal, because I can’t unsubscribe from something I haven’t been subscribed too.

const [running, setRunning] = useState(false);

useState(() => {
  if (running) {
    subscription.subscribe(...)
  } else {
    subscription.unsubscribe();
  }
});

how can I avoid that useState ran unless I pressed the button?

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>Solution :

The reason you see that function get run is that that’s what useState does, it accepts the initial state and, if that’s a function, it runs the function to get the initial state (on mount only).

You don’t use useState for (side) effects like subscribing to things, that’s just fundamentally not what it’s for. For (side) effects, generally you use useEffect:

useEffect(() => {
    if (running) {
        subscription.subscribe(/*...*/)
        return () => {
            subscription.unsubscribe();
        };
    }
}, [running]);

That does this:

  • Any time running changes, it runs the function you pass it.
  • If running is true at that point, it subscribes to the thing and returns a cleanup function that will unsubscribe from it.
  • The next time running changes, or when the component is unmounted, the cleanup callback is called, doing the unsubscription.
const { useState, useEffect } = React;

const subscription = {
    subscribe() {
        console.log("subscribed");
    },
    unsubscribe() {
        console.log("unsubscribe");
    }
};

const Example = () => {
    const [running, setRunning] = useState(false);

    useEffect(() => {
        if (running) {
            subscription.subscribe(/*...*/)
            return () => {
                subscription.unsubscribe();
            };
        }
    }, [running]);

    return <input type="button" value={running ? "stop" : "run"} onClick={() => setRunning(r => !r)} />;
};

const App = () => {
    const [showExample, setShowExample] = useState(true);

    return <div>
        {showExample ? <Example /> : <em>(example hidden)</em>}
        <div>
            <label>
                <input
                    type="button"
                    value="show/hide example"
                    onClick={() => setShowExample(e => !e)}
                />
            </label>
        </div>
    </div>;
};

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Another option is to subscribe/unsubscribe in the button click handler, but you’d still need a useEffect cleanup callback to ensure you unsubscribe on unmount. (Which can be tricky, because you can’t use the running flag to determine whether to do it, because the cleanup function will close over a stale copy of it. For that you might need a ref.)

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading