Home > Software design >  Avoid useState hook to run on initialisation
Avoid useState hook to run on initialisation

Time:02-19

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?

CodePudding user response:

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.)

  • Related