Home > Net >  How do you cleanly convert React setState callback to useEffect hooks?
How do you cleanly convert React setState callback to useEffect hooks?

Time:09-14

I can create a class component with 2 buttons, "A" and "B". Button A should set a state variable and then take some action. Button B should also set the same state variable and then take a different action. They key thing that this hinges on is the second argument to setState, which is the callback function for what action to take after the state has been set.

When I try to write this as a function component, I run into an issue. I move the callbacks into a hook (or multiple hooks), but I have no way to know which button was clicked. Furthermore, the hook does not trigger every time I click the button. The hook only triggers when the button results in a real state change (e.g. changing 5 to 5 skips the effect).

Here is a simple example:

Edit purple-cache-6zs3f6

import React from 'react';
import './styles.css';

// actionA and actionB represent things that I want to do when the
// buttons are clicked but after the corresponding state is set
const actionA = () => {
    console.log('button A thing');
};
const actionB = () => {
    console.log('button B thing');
};

// render 2 components that should behave the same way
const App = () => {
    return (
        <>
            <ClassVersion />
            <FunctionVersion />
        </>
    );
};

// the class version uses this.state
class ClassVersion extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            x: 0,
        };
    }

    render = () => {
        return (
            <div className='box'>
                Class Version
                <br />
                <button
                    type='button'
                    onClick={() => {
                        this.setState({ x: 1 }, () => {
                            // here I can do something SPECIFIC TO BUTTON A
                            // and it will happen AFTER THE STATE CHANGE
                            actionA();
                        });
                        // can't call actionA here because the
                        // state change has not yet occurred
                    }}
                >
                    A
                </button>
                <button
                    type='button'
                    onClick={() => {
                        this.setState({ x: 2 }, () => {
                            // here I can do something SPECIFIC TO BUTTON B
                            // and it will happen AFTER THE STATE CHANGE
                            actionB();
                        });
                        // can't call actionB here because the
                        // state change has not yet occurred
                    }}
                >
                    B
                </button>
                State: {this.state.x}
            </div>
        );
    };
}

// the function version uses the useState hook
const FunctionVersion = () => {
    const [x, setX] = React.useState(0);

    React.useEffect(() => {
        // the equivalent "set state callback" does not allow me to
        // differentiate WHY X CHANGED. Was it because button A was
        // clicked or was it because button B was clicked?
        if (/* the effect was triggered by button A */ true) {
            actionA();
        }
        if (/* the effect was triggered by button B */ true) {
            actionB();
        }

        // furthermore, the effect is only called when the state CHANGES,
        // so if the button click does not result in a state change, then
        // the EFFECT (AND THUS THE CALLBACK) IS SKIPPED
    }, [x]);

    return (
        <div className='box'>
            Function Version
            <br />
            <button
                type='button'
                onClick={() => {
                    // change the state and trigger the effect
                    setX(1);

                    // can't call actionA here because the
                    // state change has not yet occurred
                }}
            >
                A
            </button>
            <button
                type='button'
                onClick={() => {
                    // change the state and trigger the effect
                    setX(2);

                    // can't call actionB here because the
                    // state change has not yet occurred
                }}
            >
                B
            </button>
            State: {x}
        </div>
    );
};

export default App;

I came up with a couple ideas:

  1. Change the type of x from number to {value: number; who: string}. Change calls from setX(1) to setX({value: 1, who: 'A'}). This will allow the effect to know who triggered it. It will also allow the effect to run every time the button is clicked because x is actually getting set to a new object each time.
  2. Change the type of x from number to {value: number; callback: () => void}. Change calls from setX(1) to setX({value: 1, callback: actionA}). Same as above, but slightly different. The effect would say if(x.callback) { x.callback() }.

Both these ideas seem to work, but the problem is I don't want to keep adding an extra tag to my state variables just to keep track of which thing triggered them.

Is my idea the correct solution, or is there a better way to do this that doesn't involve "tagging" my state variables?

CodePudding user response:

How about this:

const [a, setA] = React.useState(0);
const [b, setB] = React.useState(0);

React.useEffect(() => {
        actionA();
}, [a]);

React.useEffect(() => {
        actionB();
}, [b]);

onClick={() => {
  setX(1);
  setA(a => a   1);
}}


onClick={() => {
  setX(2);
  setB(b => b   1);
}}

CodePudding user response:

Let's say you put actionA and actionB in the corresponding button A/B onClick(). Given that you WILL be updating state based on the action of this onClick, you could directly call the action functions during the onClick if you pass the new state data to the functions.

onClick={() => {
  actionA(newStateData);
  setX(newStateData);
}}

If you don't want to do this, you could use:

  • your solution of adding an identifier to the state value would be fine!
  • Create separate state values for A and B
  • Related