Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SetStateAction returned from useState hook dose not accept a second callback argument #14174

Closed
cheungseol opened this issue Nov 9, 2018 · 46 comments
Labels
Resolution: Stale Automatically closed due to inactivity Type: Question

Comments

@cheungseol
Copy link

cheungseol commented Nov 9, 2018

Do you want to request a feature or report a bug?
Question

What is the current behavior?
The SetStateAction returned from useState hook dose not accept a second callback argument. It cannot works like Class Componet's 'setState' method, which receives a callback param, and can perform the callback after this setState action updates;

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
https://codesandbox.io/s/93mkr1ywpr

What is the expected behavior?
Hopes the SetStateAction function can receive a second callback argument, and can used like 'setState' method callback.

I have read the official note:

// this technically does accept a second argument, but it's already under a deprecation warning
// and it's not even released so probably better to not define it.

If instead it's working as intended, how can I perform special action after this SetStateAction called ?

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.7.0-alpha.0

@cheungseol cheungseol changed the title SetStateAction return from useState hook dose not accept a second callback argument SetStateAction returned from useState hook dose not accept a second callback argument Nov 9, 2018
@gaearon
Copy link
Collaborator

gaearon commented Nov 9, 2018

Is there a reason you can't useEffect instead? Generally it's a better solution.

@cheungseol
Copy link
Author

Is there a reason you can't useEffect instead? Generally it's a better solution.

Since I don't want to trigger callback action every time after dispatching a SetStateAction. But 'useEffect ' might cause the effect running every time after the state changed.

@Jessidhia
Copy link
Contributor

Jessidhia commented Nov 10, 2018

Use a useRef for setting a flag saying you want the effect to run, or just do the effect directly in the function where you're calling setState, as you shouldn't be doing it in the render phase to begin with.

@gaearon
Copy link
Collaborator

gaearon commented Nov 12, 2018

Since I don't want to trigger callback action every time after dispatching a SetStateAction. But 'useEffect ' might cause the effect running every time after the state changed.

Can you describe a more specific use case please?

@therayess
Copy link

@gaearon
Hi, i also faced similar issue where i needed to add a callback for when the state is set, my scenario is this:
I make an api call, after i get the response, i update a context state item, then i take the user to a new page (pushing to history), currently, without the callback, i get a warning that says:

Warning: Can't perform a React state update on an unmounted component.

Basically, i'm running a setState on a component that has been unmounted due to the page change.

Ideally, i would like to update the context (or any callback function from parent component for example), then once that process is done, run the callback that will apply the page change.

This could've been done easily with the setState(state, [callback]).

Please advise, thanks for your time!

@therayess
Copy link

I found the solution, we can actually await the set function 👍
Thanks

@mbohgard
Copy link

mbohgard commented Dec 4, 2018

@therayess Where is it documented that the set function of useState returns a promise?

@jquense
Copy link
Contributor

jquense commented Dec 4, 2018

It doesn't, you can technically await anything in js, but that doesn't really mean anything in this case, it's not waiting until the state flushes to resolve

@therayess
Copy link

You are right, apologies for that, it worked for another reason, is there a way to achieve a state set callback with useState ?

@joshburgess
Copy link

joshburgess commented Dec 17, 2018

Being able to pass a callback to useState's returned setState would allow for greater flexibility. For example, we could implement a dispatch function that takes a callback to fire off post-reducer-update, like UpdateWithSideEffects in Reason-React: https://reasonml.github.io/reason-react/docs/en/state-actions-reducer#state-update-through-reducer

ReasonReact.UpdateWithSideEffects(state, self => unit): update the state, **then** trigger a side-effect.

Right now, it's possible to implement a Redux-like architecture with an added "hook" for running effects AFTER the reducer update with the class API, Context, and this.setState's callback param, but to accomplish the same thing with react-hooks means needing to use useRef or useEffect + extra boilerplate (setting & checking flags to control when the effect is allowed to run).

Today, in Redux-land, people use middleware libraries like redux-saga, redux-observable, redux-loop, etc. to be able to declaratively describe async effects via dispatched actions, because Redux doesn't expose an API function for running effects after a reducer update, but it's not difficult to implement this sort of functionality with the class API & setState's callback param. We could do something similar with react hooks if useState's setState or useReducer's dispatch accepted a thunk param describing a one-time effect we'd like to run.

This one-time, post-update effect feature could be implemented by offering different variations of "dispatch", like how Reason-React has SideEffects and UpdateWithSideEffects in addition to NoUpdate & Update. Having these separate versions of "dispatch" instead of a single "dispatch" dynamically overloaded with optional params is nice, because it allows us to be more explicit about our intentions & the static types being passed around.

Alternatively, instead of having different variations of dispatch, some of which allow for this post-update effect hook, imagine a simple effect system with just a single dispatch function, but where your dispatched actions could contain an extra key called effect, either at the top-level, or, perhaps, under meta for FSA users, where the value is a thunk or some other lazy async data structure we want to execute. I've personally implemented this idea using the class API & setState's callback param and found it pretty useful. If there were some way to provide a callback to useReducer's dispatch function that had access to the dispatched action, this sort of thing could be implemented in user-land with hooks.

@Jessidhia
Copy link
Contributor

Jessidhia commented Dec 17, 2018

Untested madness that I'd rather not touch...... but it's an idea, and I have no idea if it works. But it typechecks 🤷‍♀️

import React from 'react'

function useImperativeEffectsFactory(serialized = true) {
  const [, internalSetEffect] = React.useState<Promise<void> | undefined>(undefined)
  return React.useCallback<(effect: () => Promise<void> | void) => void>(
    effect => {
      internalSetEffect(current => {
        const promise = (async () => {
          try {
            if (serialized) {
              await current
              await effect()
            } else {
              await Promise.all([current, effect()])
            }
          } finally {
            internalSetEffect(current => (current === promise ? undefined : current))
          }
        })()
        return promise
      })
    },
    [internalSetEffect]
  )
}
const useImperativeEffect = useImperativeEffectsFactory()
useImperativeEffect(async () => {
  // ...
})

@joshburgess
Copy link

joshburgess commented Dec 19, 2018

@Kovensky I just tried it out, and that code results in a Too many re-renders. React limits the number of renders to prevent an infinite loop. error. Might be a simple problem, not sure. Haven't tried to debug it yet.

Anyway, I'm not saying that there aren't other ways to implement the same idea with Hooks, but it does seem like a lot of hoops need to be jumped through in order to do the same thing that currently is possible with the class API's setState just by passing in a function as an extra param.

@basbz
Copy link

basbz commented Dec 20, 2018

I think @joshburgess has a good point!
Having the callback would make it easier to express side effects like the way it's done in ReasonReact https://github.com/reasonml/reason-react/blob/de0803fa5784e434e3d9da374ec3e4e8a7653f12/src/ReasonReact.re/#L591
or in React ReComponent https://github.com/philipp-spiess/react-recomponent/blob/6c75e1ce99238af0db3389c25a2ca0cee5fa8207/src/re.js#L96
And would enable isolating state even more from classes or components that us it.

@malerba118
Copy link

I agree, i'd like a callback option. Or taking it one step further, I'd love to see a promise returned. That's one thing that's always irked me about setState.

@joshburgess
Copy link

For whatever it's worth, it's probably based on what reason-react did, but purescript-react-basic also went with this approach.

data StateUpdate props state action
  = NoUpdate
  | Update               state
  | SideEffects                (Self props state action -> Effect Unit)
  | UpdateAndSideEffects state (Self props state action -> Effect Unit)

https://github.com/lumihq/purescript-react-basic/blob/master/src/React/Basic.purs#L165-L169

@joshburgess
Copy link

joshburgess commented Jan 3, 2019

Just posting an update here for others, as I hadn't seen this before until just now. It looks like they are already aware that people want this and have some ideas about how it might be possible.

See the Missing APIs section here: reactjs/rfcs#68 (comment)

Relevant section in a screenshot:

screen shot 2019-01-02 at 7 55 20 pm

@bebbi
Copy link

bebbi commented Feb 21, 2019

@gaearon I have this use-case which perhaps matches the issue:

const Component = ({ extValue }) => {
  const [value, setValue] = useState(extValue)

  useEffect(() => {
    // Normally, value is changed within this Component.
    // However, whenever `extValue` updates, need to reset value to extValue.
    // As soon as flushed, run 'onExternalValueUpdate'
    setValue(extValue)
      .then(onExternalValueUpdate)
  }, [extValue] )

  // Internal updates won't trigger `onExternalValueUpdate`
  return (<Something value={value} setValue={setValue} />)
}

@malerba118
Copy link

@bebbi Not arguing against a callback option as it would be a simpler solution, but I believe you could accomplish this by tracking extVal's state in the child.

const Parent = props => {
  let [extVal, setExtVal] = useState(0);

  useEffect(() => {
    setTimeout(() => {
      setExtVal(prevVal => prevVal + 1);
    }, 5000);
  }, []);

  return <Child extVal={extVal} />;
};

const Child = props => {
  let [state, setState] = useState({
    val: props.extVal,
    extVal: props.extVal
  });

  useEffect(
    () => {
      setState({
        val: props.extVal,
        extVal: props.extVal
      });
    },
    [props.extVal]
  );

  useEffect(
    () => {
      // We know state.val is also done setting
      console.log(state);
    },
    [state.extVal]
  );

  return <div>{state.val}</div>;
};

https://codesandbox.io/s/xjowmro8yz

@gaearon
Copy link
Collaborator

gaearon commented Feb 22, 2019

@bebbi Sorry, it's not clear why you need it. A full codesandbox showing the full use case (including why a parent needs such a callback) would be useful.

@davidalekna
Copy link

davidalekna commented Feb 22, 2019

Hi @gaearon, following method example comes from render props class component. I'd like to refactor it with react hooks. All of the methods on this class component has a callback when on setState is called. It passes a currently affected state so that the end user could do something if that action takes place.

selectAll = ({
  type = DataBrowser.stateChangeTypes.selectAll,
  items,
} = {}) => {
  this.internalSetState(
    {
      type,
      selectAllCheckboxState: true,
      checked: items,
    },
    () => this.props.onSelectAll(this.getState().checked),
  );
};

and here is the internalSetState function

  internalSetState = (changes, callback = () => {}) => {
    let allChanges;
    this.setState(
      currentState => {
        const combinedState = this.getState(currentState);
        return [changes]
          .map(c => (typeof c === 'function' ? c(currentState) : c))
          .map(c => {
            allChanges = this.props.stateReducer(combinedState, c) || {};
            return allChanges;
          })
          .map(({ type: ignoredType, ...onlyChanges }) => onlyChanges)
          .map(c => {
            return Object.keys(combinedState).reduce((newChanges, stateKey) => {
              if (!this.isControlledProp(stateKey)) {
                newChanges[stateKey] = c.hasOwnProperty(stateKey)
                  ? c[stateKey]
                  : combinedState[stateKey];
              }
              return newChanges;
            }, {});
          })
          .map(c => (Object.keys(c || {}).length ? c : null))[0];
      },
      () => {
        this.props.onStateChange(allChanges, this.state);
        callback();
      },
    );
  };

It would be kinda hard to replicate all this on a codesandbox but if nescessary I could produce something?

This is the component that is in the process of refactoring

Thanks

@bebbi
Copy link

bebbi commented Feb 22, 2019

@gaearon Here is a fiddle example of what I was trying to simplify.

@malerba118
Copy link

malerba118 commented Feb 22, 2019

Not necessarily recommending this approach as it's possibly an anti-pattern, but for the people wanting this functionality, I suggest something like this as a solution (https://codesandbox.io/s/qq672lp6xq):

const useStateWithPromise = defaultVal => {
  let [state, setState] = useState({
    value: defaultVal,
    resolve: () => {}
  });

  useEffect(
    () => {
      state.resolve(state.value);
    },
    [state]
  );

  return [
    state.value,
    updater => {
      return new Promise(resolve => {
        setState(prevState => {
          let nextVal = updater;
          if (typeof updater === "function") {
            nextVal = updater(prevState.value);
          }
          return {
            value: nextVal,
            resolve
          };
        });
      });
    }
  ];
};

const App = props => {
  let [state, setState] = useStateWithPromise(0);

  const increment = () => {
    setState(prevState => prevState + 1).then(window.alert);
  };

  return (
    <div>
      <p>{state}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

@revskill10
Copy link

revskill10 commented Mar 1, 2019

Here's my solution

import { useState } from 'react'
const useStateWithEffects = (props, onValueChanged) => {
  let [value, setValue] = useState(props)

  const setState = (newValue) => {
    value = newValue
    setValue(value)
    onValueChanged(value)
  }

  return [value, setState]
}

export default useStateWithEffects

Usage:

const [value, setValue] = useStateWithEffects(10, async () => console.log('changed') )

@ulrichb
Copy link

ulrichb commented Mar 14, 2019

So ... are there plans to add a callback-parameter (or even better returning a Promise) within React?

@Tahseenm
Copy link

Tahseenm commented Mar 19, 2019

Solution

import React from 'react'

/* :: (any, ?Function) -> Array<any> */
export const useState = (initialState, callback = () => { }) => {
  const [ state, setState ] = React.useState(initialState)
  const totalCalls = React.useRef(0)

  React.useEffect(() => {
    if (totalCalls.current < 1) {
      totalCalls.current += 1
      return
    }

    callback(state)
  }, [ state ])

  return [ state, setState ]
}

Usage

const App = () => {
  const onCountChange = count => {
    console.log('Count changed', count)
  }

  const [count, setCount] = useState(10, onCountChange)

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count => count + 1)}>+</button>
    </>
  )
}

@Tahseenm
Copy link

@gaearon useEffect() runs after every render AFAIK. With the callback I needed it to run only when the state changed.

@zenVentzi
Copy link

@Tahseenm your solution provides a "global" callback for all setCount() calls. I think some people need a solution where they can provide a custom callback for each and every setCount() call. Which allows greater flexibility

@bebbi
Copy link

bebbi commented Mar 19, 2019

@Tahseenm

useEffect() runs after every render

Here is a very good explanation for useEffect() and general hooks logic - by Dan. I think it should be a mandatory pre-read for everyone posting in this thread (including me).

@Tahseenm
Copy link

@zenVentzi

useState with Global + setState callback

/* :: (any, ?Function) -> Array<any> */
const useState = (initialState, callback = () => {}) => {
  const [state, setState] = React.useState(initialState);

  /** @NOTE: Handle callback to run for every state change */
  const totalCalls = React.useRef(0);
  React.useEffect(
    () => {
      if (totalCalls.current < 1) {
        totalCalls.current += 1;
        return;
      }

      callback(state);
    },
    [state]
  );

  /** @NOTE: Handle setState callback */
  const lookup = React.useRef([]);
  React.useEffect(
    () => {
      const entry = lookup.current.find(([stateSet]) => state === stateSet);

      if (Array.isArray(entry)) {
        const [, callback] = entry;
        callback(state);
      }

      lookup.current = [];
    },
    [state]
  );
  const $setState = (nextStateOrGetter, callback) => {
    setState(nextStateOrGetter);

    if (typeof callback !== "function") {
      return;
    }

    const nextState =
      typeof nextStateOrGetter === "function"
        ? nextStateOrGetter(state)
        : nextStateOrGetter;
    lookup.current.push([nextState, callback]);
  };

  return [state, $setState];
};

Usage

const App = () => {
  const onCountChange = count => {
    console.log("Count changed", count);
  };

  const [count, setCount] = useState(10, onCountChange);

  const decrement = () => setCount(count - 1, () => console.log("dec"));
  const increment = () => setCount(count + 1, () => console.log("inc"));

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </>
  );
};

@Tahseenm
Copy link

Tahseenm commented Mar 19, 2019

@bebbi Thanks will check it out. Not sure if we should make it a mandatory pre-read for this thread

@technoY2K
Copy link

technoY2K commented Mar 25, 2019

Thanks @bebbi, but I think most of us legitimately just need a function to run after a state change so we can do something with the most recent state values. For example, how can I make an API call after updating state and guarantee that its the latest values? Before it was like the following.

this.setState({ name: 'FakeMan', }, () => somefakeCall(this.state.name))

Currently with useState that is not possible. In this scenario useEffect won't quite work. I don't want to make the API call on the initial render and I might not necessary want to make the call every time the name state changes, I just want to make this call in a very specific instance.

@MohamedLamineAllal
Copy link

MohamedLamineAllal commented Apr 2, 2019

@gaearon
Hi Dan!

Is there a reason you can't useEffect instead? Generally it's a better solution.

Can you explain, why it's a better solution?

Also why it is not implemented ? (as for my understanding, it should be just one call at the right moment, when the state is updated. At least we can do it when useEffect is triggered. Typically using a promise, which will be resolved at the right moment). I bet it's related to optimization, and maybe consistency. But what difference it make from handling it in useEffect).

And what do you think about the statement 'having the callback (||promise). Add a lot of flexibility.' ?

Thank you a lot.

UPDATE
For why it's a better solution. What i can understand, is that using multiple useEffec (separate memory cells)t, tracking the changes of the specific variables. Make the separation easy. And so the after update can be handled just there.

@MohamedLamineAllal
Copy link

MohamedLamineAllal commented Apr 3, 2019

Here a pattern to mimic the after update behavior. It's short and clean. (it use promises)

class that help with that

class UpdateHandler {
    queue =[];
    
    flash(states) {
        while(this.queue.length > 0) {
            this.queue.shift()(states);
        }
    }

    wait () {
        return new Promise (resolve => {
            this.queue.push(resolve);
        });
    }
}

use:

 const update = new UpdateHandler();

in hook

        useEffect(() => {
		update.flash(settings);
	}, [settings])

in some function where we use setState

async function someFunc(value, setState) {
     setState(value);
     let newState = await update.wait(); // whatever was passed to the flash method
     // use it as you like
}

What do you think ?

@limpt
Copy link

limpt commented Aug 1, 2019

@bebbi Sorry, it's not clear why you need it. A full codesandbox showing the full use case (including why a parent needs such a callback) would be useful.

https://codesandbox.io/s/objective-pare-42mn4

A callback for setState would be useful in this case because I don't want the calendar field to be set every time the text input field update.
I could use the formatted value to set the calendar field but not sure if there's a better way.

@SylarRuby
Copy link

What's wrong with this:

const [increment, setIncrement] = useState(0);
<button onClick={() => setIncrement(increment + 1)}>Num is: {increment}</button>

@kaung8khant
Copy link

kaung8khant commented Sep 13, 2019

My solution
This is also work for dispatch, but state value is not updated yet on callback function.

import React, { useState } from "react";

const CallBackTest = () => {
  const [trueFalse, setTrueFalse] = useState(false);

  const callBacksetState = async (data, callBackFunc) => {
    await setTrueFalse(data);
    callBackFunc(data);
  };

  const func = data => {
    alert(data + " " + trueFalse);
  };

  //pass function func to callBacksetState function
  return  <button onClick={() => callBacksetState(true, func)}>Click Me!</button>;
};

export default CallBackTest;

@verbart
Copy link

verbart commented Oct 25, 2019

@arascan35

import React from 'react';

export const DeleteFromArray = ({ deletionIndex }) => {
  const [ emails, setEmails ] = React.useState([]);

  const handleDelete = () =>  {
    setEmails((emails) => emails.filter((email, index) => index !== deletionIndex));
  };

  return <button onClick={handleDelete}>Delete</button>;
};

@ghost
Copy link

ghost commented Nov 28, 2019

Thanks @bebbi, but I think most of us legitimately just need a function to run after a state change so we can do something with the most recent state values. For example, how can I make an API call after updating state and guarantee that its the latest values? Before it was like the following.

this.setState({ name: 'FakeMan', }, () => somefakeCall(this.state.name))

Currently with useState that is not possible. In this scenario useEffect won't quite work. I don't want to make the API call on the initial render and I might not necessary want to make the call every time the name state changes, I just want to make this call in a very specific instance.

@jhamPac
Good point. As in below snippet: I want console.log("Fetch") to be written when state is updated after btn1 is clicked and not when btn2. With useEffect it will be cumbersome to implement such scenario.
@gaearon

class App extends React.Component {
  state = { flag: false };
  render() {
    return (
      <div className="App">
        <button
          onClick={
            (() => {
              this.setState(ps => ({ flag: !ps.flag }));
            },
            () => {
              console.log("Fetch");
            })
          }
        >
          btn1
        </button>
        <button
          onClick={() => {
            this.setState(ps => ({ flag: !ps.flag }));
          }}
        >
          btn2
        </button>
      </div>
    );
  }
}

@zmhweb
Copy link

zmhweb commented Dec 7, 2019

Not necessarily recommending this approach as it's possibly an anti-pattern, but for the people wanting this functionality, I suggest something like this as a solution (https://codesandbox.io/s/qq672lp6xq):

const useStateWithPromise = defaultVal => {
  let [state, setState] = useState({
    value: defaultVal,
    resolve: () => {}
  });

  useEffect(
    () => {
      state.resolve(state.value);
    },
    [state]
  );

  return [
    state.value,
    updater => {
      return new Promise(resolve => {
        setState(prevState => {
          let nextVal = updater;
          if (typeof updater === "function") {
            nextVal = updater(prevState.value);
          }
          return {
            value: nextVal,
            resolve
          };
        });
      });
    }
  ];
};

const App = props => {
  let [state, setState] = useStateWithPromise(0);

  const increment = () => {
    setState(prevState => prevState + 1).then(window.alert);
  };

  return (
    <div>
      <p>{state}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

function useStateWithPromise<T>(defaultVal: T) {
    let [state, setState] = useState({
        value: defaultVal,
        resolve: (value: T) => { }
    });

    useEffect(() => {
        state.resolve(state.value);
    },
        [state]
    );

    return [
        state.value,
        (updater: (value: T) => any) => {
            return new Promise(resolve => {
                setState((prevState) => {
                    let nextVal = updater;
                    if (typeof updater === "function") {
                        nextVal = updater(prevState.value);
                    }
                    return {
                        value: nextVal,
                        resolve
                    };
                });
            });
        }
    ];
};


image

How do I use generics?

@hnsylitao
Copy link

Not necessarily recommending this approach as it's possibly an anti-pattern, but for the people wanting this functionality, I suggest something like this as a solution (https://codesandbox.io/s/qq672lp6xq):

const useStateWithPromise = defaultVal => {
  let [state, setState] = useState({
    value: defaultVal,
    resolve: () => {}
  });

  useEffect(
    () => {
      state.resolve(state.value);
    },
    [state]
  );

  return [
    state.value,
    updater => {
      return new Promise(resolve => {
        setState(prevState => {
          let nextVal = updater;
          if (typeof updater === "function") {
            nextVal = updater(prevState.value);
          }
          return {
            value: nextVal,
            resolve
          };
        });
      });
    }
  ];
};

const App = props => {
  let [state, setState] = useStateWithPromise(0);

  const increment = () => {
    setState(prevState => prevState + 1).then(window.alert);
  };

  return (
    <div>
      <p>{state}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

function useStateWithPromise<T>(defaultVal: T) {
    let [state, setState] = useState({
        value: defaultVal,
        resolve: (value: T) => { }
    });

    useEffect(() => {
        state.resolve(state.value);
    },
        [state]
    );

    return [
        state.value,
        (updater: (value: T) => any) => {
            return new Promise(resolve => {
                setState((prevState) => {
                    let nextVal = updater;
                    if (typeof updater === "function") {
                        nextVal = updater(prevState.value);
                    }
                    return {
                        value: nextVal,
                        resolve
                    };
                });
            });
        }
    ];
};

image

How do I use generics?

Good reference

@ryanhornberger
Copy link

ryanhornberger commented Feb 27, 2020

@gaearon our company ran into this issue today attempting to upgrade (to hooks) a library that employed the callback on setState.

After reading this thread we looked through our code for every instance where we were using it.

We concluded that in nearly every case we were simply (and likely incorrectly) using this to ensure the form only submitted once and could not submit again until after an API call was complete.

Our solution to this problem looks similar to the solution @Tahseenm submitted but we gave it a more appropriate name and usage. This might be useful to build right in to react.

The entire solution follows:

import { useState, useEffect, useCallback } from "react"

// React Hook - useAsyncCallbackWithSemaphore
//
// Use this hook to create a callback capable of triggering an async operation
// (such as an api call ) that will only be triggered once.
//
// You will receive in return an array that contains
//    - a callback function
//
//        name this function anything you want, and pass it to your "onClick"
//
//    - a state variable containing the semaphore status
//
//        name this variable anything you want, use it to disable inputs and
//        signal to the user that their command is received and processing.
//
// Example:
//
//    export default (props) => {
//      const [userName, setUserName] = useState('')
//      const [saveUserName, isSavingUsername] = useAsyncCallbackWithSemaphore(
//          async () => {
//              try {
//                  await mockApiCall(userName)
//              }
//              catch(e) {
//                  //TODO handle errors
//              }
//          },
//          [userName]
//      )
//
//      return (
//          <div>
//              <MockUsernameForm
//                  value={userName}
//                  onChange={setUserName}
//                  disabled={isSavingUsername}
//              />
//              <button
//                  onClick={saveUserName}
//                  disabled={isSavingUsername}
//              >
//                  Save
//              </button>
//          </div>
//      )
//    }
//

const useAsyncCallbackWithSemaphore = (callOnceFn, watchingVars) => {
    // store the semaphore in state
    const [semaphore, setSemaphore] = useState(false)

    // use the state system to delay triggering the async function until after
    // the UI has had a chance to disable inputs.
    //
    // also only allow this call to happen on the first try if the user presses
    // the button multiple times
    useEffect(() => {
        //if semaphore > 1 this is a repeat call that should be ignored
        if(semaphore === 1) {
            // we need to await the async function so that we can remove the
            // semphore on completion
            (async () => {
                await callOnceFn()
                // async function complete. Remove semaphore
                setSemaphore(false)
            })()
        }
    }, [semaphore]) // this useEffect will only trigger if the semaphore changes

    return [
        
        // result[0] = a callback function that can be used in "onClick" or
        // other triggers
        useCallback(() => {
            setSemaphore(semaphore + 1)
        }, , [...watchingVars, semaphore]),
        
        // result[1] = a boolean indicating if the semaphore has been triggered
        // to be used by rendering components down-stream
        (semaphore > 0)
        
    ]
}

export default useAsyncCallbackWithSemaphore

@stale
Copy link

stale bot commented May 30, 2020

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@stale stale bot added the Resolution: Stale Automatically closed due to inactivity label May 30, 2020
@stale
Copy link

stale bot commented Jun 6, 2020

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

@stale stale bot closed this as completed Jun 6, 2020
@lishichao1002
Copy link

type Action<T> = (preState?: T) => T;
type Dispatch<T> = (state: T | Action<T>) => Promise<T>;

function useMyState<T>(initState: T): [T, Dispatch<T>] {
    const [state, setState] = useState(initState);
    const [{ __state, __resolve }, __setState] = useState<{ __state?: T, __resolve?: (t: T) => void }>({});

    const mySetState: Dispatch<T> = (state: T | Action<T>) => {
        return new Promise<T>((resolve) => {
            if (typeof state === 'function') {
                setState((preState: T) => {
                    const __state = (state as Action<T>)(preState);
                    __setState({
                        __state,
                        __resolve: resolve
                    });
                    return __state;
                });
            } else {
                const __state = state;
                __setState({
                    __state,
                    __resolve: resolve
                });
                setState(__state);
            }
        });
    };

    useEffect(() => {
        if (__state && __resolve) {
            __resolve(__state);
            __setState({});
        }
    }, [state]);

    return [state, mySetState];
}

function App() {
    const [state, setState] = useMyState(0);

    useEffect(() => {
        console.warn('before', state);
        setState(1).then((state) => {
            console.warn('after', state);
        });
    }, []);

    return (
        <div className="App"></div>
    );
}

@jernchr11
Copy link

Is there a solution for this yet?

@emrahaydemir
Copy link

Unfortunately, you should use setState. useState is not working with second parameter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Resolution: Stale Automatically closed due to inactivity Type: Question
Projects
None yet
Development

No branches or pull requests