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