Home > Mobile >  Use onClick event (or trigger of a function) as useEffect dependency
Use onClick event (or trigger of a function) as useEffect dependency

Time:03-06

As I understand it, passing an array of states as dependencies in a useEffect function results in the function only being called when those specific states change.

I have a function (handleClick) that is called in response to an onClick event. This triggers a change in state. I then wish call a useEffect function that makes additional changes to that state, depending on its content. I must call the additional function inside a useEffect, as the component must re-render (and the state update) for this functions to work as intended.

Given that this additional function also changes the state, I cannot use the state as a dependency for the useEffect (as it will trigger an infinite loop). Therefore, I was wondering if it were possible to trigger the useEffect using a the onClick event (or something along those lines).

The below code is an extract. I apologise that it is not really reproducible.

function handleClick(event){
        let button = event.target.id // Set button to element (button) id

        if (button !== 'all'){ // Executes if button is not the "all subjects" button
            if (subjects[button] === 'active'){ // If button is currently active, set to inactive
                setSubjects(prevSubjects => ({...prevSubjects, [button]: 'inactive'}))
            } else { // If button is currently inactive, set to active
                setSubjects(prevSubjects => ({...prevSubjects, [button]: 'active'}))
            }
        } else { // Executes if button is the "all subjects" button
            if (subjects["all"] === 'active'){ // If "all" button is currently active...
                for (let subject in subjects){ // ...inactivate all subjects
                    setSubjects(prevSubjects => ({...prevSubjects, [subject]: 'inactive'}))
                    setSubjects(prevSubjects => ({...prevSubjects, all: 'inactive'}))
                } 
            } else { // If "all" button is not currently active...
                for (let subject in subjects){ // ...activate all subjects
                    setSubjects(prevSubjects => ({...prevSubjects, [subject]: 'active'}))
                    setSubjects(prevSubjects => ({...prevSubjects, all: 'active'}))
                } 
            }
        }
    }

    function checkAll(){
        setSubjects(prevSubjects => ({...prevSubjects, all: 'active'})) // Set "all" button to active by default

        for (let subject in subjects){ // Iterate through object to find inactive subjects
            if (subject !== "all"){ // Excludes "all" button
                if (subjects[subject] === 'inactive'){ // If any subject is inactive...
                    setSubjects(prevSubjects => ({...prevSubjects, all: 'inactive'})) // ...inactivate "all" button
                }
            }
        }
    }

Here I have the useEffect – I have put the handleClick function as the dependency, to indicate what I am intending.

useEffect(() => checkAll(), [handleClick])

EDIT

I originally had my code structured as such:

function handleClick(event){
        let button = event.target.id // Set button to element (button) id

        if (button !== 'all'){ // Executes if button is not the "all subjects" button
            if (subjects[button] === 'active'){ // If button is currently active, set to inactive
                setSubjects(prevSubjects => ({...prevSubjects, [button]: 'inactive'}))
            } else { // If button is currently inactive, set to active
                setSubjects(prevSubjects => ({...prevSubjects, [button]: 'active'}))
            }

            setSubjects(prevSubjects => ({...prevSubjects, all: 'active'})) // Set "all" button to active by default

            for (let subject in subjects){ // Iterate through object to find inactive subjects
                if (subject !== "all"){ // Excludes "all" button
                    if (subjects[subject] === 'inactive'){ // If any subject is inactive...
                        setSubjects(prevSubjects => ({...prevSubjects, all: 'inactive'})) // ...inactivate "all" button
                    }
                }
            }

        } else { // Executes if button is the "all subjects" button
            if (subjects["all"] === 'active'){ // If "all" button is currently active...
                for (let subject in subjects){ // ...inactivate all subjects
                    setSubjects(prevSubjects => ({...prevSubjects, [subject]: 'inactive'}))
                    setSubjects(prevSubjects => ({...prevSubjects, all: 'inactive'}))
                } 
            } else { // If "all" button is not currently active...
                for (let subject in subjects){ // ...activate all subjects
                    setSubjects(prevSubjects => ({...prevSubjects, [subject]: 'active'}))
                    setSubjects(prevSubjects => ({...prevSubjects, all: 'active'}))
                } 
            }
        }
    }

'subjects' is a object (state variable) structured as such:

const [subjects, setSubjects] = useState({all: inactive, button1: inactive, button2: inactive, button3: inactive, button4: inactive, button5: inactive, button6: inactive})

When a button is clicked, the ID of which corresponds to one of the keys in the object, it switches from active to inactive (or vice versa). If all buttons are active, the 'all' button is set to active also. If not, the 'all' button is inactive. The checkAll function in the original question pertains to this test. By default, it is set to active, then the buttons are iterated through to check if any are inactive.

However, I would find that with the above (edited) code, if all but one buttons were active and I activated the last button, the all button would not activate (and if I console.logged the state, it would state that the last button was still inactive). If I then inactivated a button, the 'all' button would activate. Hence, the 'all' button would always seem to lag behind by one render.

CodePudding user response:

If you want to run the useEffect() hook on an onClick() event, you can create a state and update the state on click event, and use the state as a dependency to the useEffect().

function App() {
const [name, setName] = useState("");
const [inpName, setInpName] = useState("");

useEffect(() => {
    console.log("here it ranned")
}, [name]);

 return (
  <div className="App">
   <p>{name}</p>
    <input onChange={(e) => setInpName(e.target.value)} />
    <button onClick={() => setName(inpName)}>click</button>
  </div>
 );
}

In this example the useEffect() hook will run once when the component mounts and after whenever the name state is updated using click event.

CodePudding user response:

What I'm seeing from your edit, you were trying to set state multiple times in one function. The state updates should happen in one call to the setter within the context of a function. So I took your logic and reduced to one if/else statement that sets state only once.

function handleClick(event){
    let button = event.target.id
    function anyActive() {
        for (let subject in subjects){
            if (subject !== "all" && subject === 'inactive') {
                return true
            }
        }
    }

    if (button !== 'all') {
        setSubjects(prevSubjects => ({
            ...prevSubjects,
            all: anyActive() ? "inactive" : "active",
            [button]: button === 'active' ? 'inactive' : 'active'
        }))
    } else {
        let subjectsCopy
        for (let subject in subjects) {
            subjectsCopy[subject] = subjects["all"] === "inactive" ? "active" : "inactive"
        }
        setSubjects(subjectsCopy)
    }
}

I made use of the ternary operator to set 'active' or 'inactive' state based on your previous if statements. I also extracted the loop into a helper function to return true if one of the non-all buttons are inactive This is what allows me to get it all done in one state set.

  • Related