Kayote
Kayote

Reputation: 15617

React - Substitute for `setState` Callback in Functional Components?

We have migrated to 'React Functional Components' instead of 'Class based Component'. I cannot find the substitute logic for setState callback function. I.e, I have a functional component with state, and I want to create an event handler function that mutates the state multiple times in sequence, the caveat being that I dont know the current value of state (it may be true/false). The following example may make more sense.

const Example = () => {
  const [ openDoor, setOpenDoor ] = useState(false); 
  // the following handler should swich 'openDoor' state to inverse of
  // current state value. Then after setTimeout duration, inverse it again
  const toggleOpenDoor = () => {
  setOpenDoor(!openDoor);
  // within setTimeout below, '!openDoor' does not work because it still
  // receives the same value as above because of async nature of 
  // state updates
  setTimeout(() => setOpenDoor(!openDoor), 500)
  }
  return(...);
}

In class based components, we had callback argument which would update state after previous update. How do I achieve the same in the above functional component using state hook?

Upvotes: 2

Views: 2621

Answers (6)

Max
Max

Reputation: 2036

I'll tell you that it works pretty much in the same way as this.setState, you just a pass a callback function which takes previous state as a parameter and returns new state(docs)

const Example = () => {
  const [openDoor, setOpenDoor] = useState(false); 

  const toggleOpenDoor = () => {
    setOpenDoor(!openDoor);
    setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
  }
  return(...);
}

In order for you know when it changes you can use useEffect callback, which's gonna be called each time something changes in the dependencies array(docs)

const Example = () => {
  const [openDoor, setOpenDoor] = useState(false); 

  useEffect(() => {
    console.log('openDoor changed!', openDoor)
  }, [openDoor])

  const toggleOpenDoor = () => {
    setOpenDoor(!openDoor);
    setTimeout(() => setOpenDoor(prevDoor => !prevDoor), 500)
  }
  return(...);
}

Upvotes: 1

fjplaurr
fjplaurr

Reputation: 1950

I wonder if useEffect is the best solution. Specially when calling setTimeout within useEffect is going to cause an infinite loop since every time we call setOpenDoor, the app renders and then useEffect is called calling again a setTimeOut that will call a setOpenDoor function... Graphically:

setTimeout -> setOpenDoor -> useEffect -> setTimeout -> ... hell  

Of course you could use an if statement wihin useEffect the same way that @ksav suggested but that does not accomplish one requirement of @Kayote:

I dont know the current value of state (it may be true/false)

Here is a solution that works without useEffect and accomplish the requirement stated above:

Code working in codesandbox

There, see the importance of this piece of code:

const toggleOpenDoor = () => {
  setOpenDoor(!openDoor);
  setTimeout(() => setOpenDoor(openDoor => !openDoor), 500);
};

Since we are using setTimeout, we need to pass callback to setOpenDoor instead of the updated state. This is because we want to send the 'current' state. If we sent the new state instead, by the time that setTimeOut processes that state, it will have changed (because we did it before setTimeOut executes its callback with setOpenDoor(!openDoor);) and no changes will be made.

Upvotes: 2

ksav
ksav

Reputation: 20821

import React, { useState, useEffect } from "react";

const Example = () => {
  const [openDoor, setOpenDoor] = useState(false);
  const toggleOpenDoor = () => {
    setOpenDoor(!openDoor);
  };
  useEffect(() => {
    console.log(openDoor);
    if (openDoor) {
      setTimeout(() => setOpenDoor(!openDoor), 1500);
    }
  }, [openDoor]);
  return (
    <>
      <button onClick={toggleOpenDoor}>Toggle</button>
      <p>{`openDoor: ${openDoor}`}</p>
    </>
  );
};

export default Example;

Codesandbox

Upvotes: 1

Dennis Vash
Dennis Vash

Reputation: 53874

You should just using setTimeout within useEffect callback:

const App = () => {
  const [openDoor, setOpenDoor] = useState(false);

  const toggle = () => setOpenDoor(prevOpen => !prevOpen);

  useEffect(() => {
    const id = setTimeout(() => toggle(), 1000);
    return () => clearTimeout(id);
  }, [openDoor]);

  return <Container>isOpen: {String(openDoor)}</Container>;
};

Edit kind-dirac-b68xf

Upvotes: 1

Vinod S Pattar
Vinod S Pattar

Reputation: 91

You can use useEffect hook to achieve this.

setOpenDoor(!openDoor);

useEffect(() => {
   // Here your next setState function
}, [openDoor]);

For more information on hooks please check out https://reactjs.org/docs/hooks-effect.html

Upvotes: 1

Siva Kondapi Venkata
Siva Kondapi Venkata

Reputation: 11001

You can use useEffect hook to see when the state change happend.

useEffect(() => { 
  // do something
  console.log('openDoor change', openDoor)
}, [openDoor]);

Upvotes: 2

Related Questions