52

I have function which dispatched an action. I would like to display a loader before and after the action. I know that react composing the object passed to setState. the question is how can I update the property in async way:

handleChange(input) {
    this.setState({ load: true })
    this.props.actions.getItemsFromThirtParty(input)
    this.setState({ load: false })
}

Basically, it all worked great if I put this property as part of the application state (using Redux), but I really prefer to bring this property to the component-state only.

Janez Kuhar
  • 3,705
  • 4
  • 22
  • 45
Chen
  • 2,958
  • 5
  • 26
  • 45
  • How about this.props.dispatch(getItemsFromThirtParty()).then(/* whatever */) on the component level? I mean, as an author of redux wrote here - http://stackoverflow.com/a/33168143/6538824 Do you really need a redux there? – Giorgi Khorguani Apr 12 '17 at 13:15
  • So how are you planning to re-render the component when the action completes? – hazardous Apr 12 '17 at 13:21
  • Giorgi, it's exactly the point. I don't want to use redux for the "loader" state. Of course I need redux for the application state (the actual DATA). The loader belongs to the component only, and it shouldn't be a pert of the application data. I just wish to display it when i'm fetching the data, and hide it after i'm getting the response. – Chen Apr 12 '17 at 13:25

8 Answers8

104

you can wrap the setState in a Promise and use async/await as below

setStateAsync(state) {
    return new Promise((resolve) => {
      this.setState(state, resolve)
    });
}

async handleChange(input) {
    await this.setStateAsync({ load: true });
    this.props.actions.getItemsFromThirtParty(input);
    await this.setStateAsync({ load: false })
}

Source: ASYNC AWAIT With REACT

Tejas Rao
  • 1,149
  • 2
  • 7
  • 2
  • 8
    I took it a step further and did `const asyncSetState = instance => newState => new Promise(resolve => instance.setState(newState, resolve));` which you can use in any component like so ... `await asyncSetState(this)({ mystuff: false });` – Ben Feb 19 '19 at 22:40
  • 2
    Or keep it _simple_ :-) You can add this to your util to reuse `export function setStateAsync(state, that) { return new Promise((resolve) => { that.setState(state, resolve); }); }` And call `await setStateAsync(, this)` – Bobz Apr 21 '20 at 03:45
48

Wrap the rest of your code in the callback of the first setState:

handleChange(input) {
  this.setState({
    load: true
  }, () => {
    this.props.actions.getItemsFromThirtParty(input)
    this.setState({ load: false })
  })
}

With this, your load is guaranteed to be set to true before getItemsFromThirtParty is called and the load is set back to false.

This assumes your getItemsFromThirtParty function is synchronous. If it isn't, turn it into a promise and then call the final setState within a chained then() method:

handleChange(input) {
  this.setState({
    load: true
  }, () => {
    this.props.actions.getItemsFromThirtParty(input)
      .then(() => {
        this.setState({ load: false })
      })
  })
}
James Donnelly
  • 126,410
  • 34
  • 208
  • 218
4

Here's a typescript implementation of an "async-await" setState:

async function setStateAsync<P, S, K extends keyof S>(
  component: Component<P, S>,
  state:
    ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) |
    Pick<S, K> |
    S |
    null
) {
  return new Promise(resolve => component.setState(state, resolve));
}
Tobiq
  • 2,489
  • 19
  • 38
  • I get error: `Argument of type '(value: unknown) => void' is not assignable to parameter of type '() => void'.ts(2345)` – JM217 Dec 12 '21 at 16:20
3

The previous answers don't work for Hooks. In this case you get the following error when passing a second argument to setState

Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().

As the error message says, you need to use useEffect instead in this case (see also this discussion for more detailed information)

Jonathan
  • 517
  • 1
  • 6
  • 12
2

Here's what you can do...

  1. Change your action to take in a onFetchComplete callback, along with the input.
  2. Change your handleChange to -

    handleChange(input) {
        this.setState({ load: true }, ()=>
            this.props.actions.getItemsFromThirtParty(input,
            ()=>this.setState({ load: false }))
        );
    }
    

This will ensure the action processor code can invoke back your state change callback even if it's not written in a promise based fashion.

hazardous
  • 10,627
  • 2
  • 40
  • 52
  • Interesting. But Actually prefer the promise fashion. thanks for advising – Chen Apr 12 '17 at 13:30
  • 1
    Yes Promise works better. In some scenarios like Reflux, its not easily possible to return results from actions, so you can't use promises there. Also, you must remember to catch them or else you risk running the spinner forever :). Having some actions return non-promise and others promise is also something that should be clearly identified, say by their names. – hazardous Apr 12 '17 at 13:32
  • 1
    Catching errors- never to be missed. And couldn't agree more about the naming! – Chen Apr 13 '17 at 06:24
0

A small update- using promises for the action creators and async/await works great, and it makes the code even cleaner, compared to the "then" chaining:

(async () => {
   try {
    await this.props.actions.async1(this.state.data1);
    await this.props.actions.async2(this.state.data2) 
    this.setState({ load: false );
   } catch (e) {
    this.setState({load: false, notify: "error"});
   }
})();

Of course it is a matter of taste.

EDIT : Added missing bracket

Community
  • 1
  • 1
Chen
  • 2,958
  • 5
  • 26
  • 45
  • 2
    Move `this.setState({ load: false );` in `finally`if you don't need to notify error, your code will be more succinct ;). – hazardous Apr 13 '17 at 07:58
0

I know this is about class components... But in functional components I do this to synchronously set the state:

const handleUpdateCountry(newCountry) {
    setLoad(() => true);
    setCompanyLocation(() => newCountry);
    setLoad(() => false);
}

Just worked for my automatic country detection that should set the form to dirty.

CodingYourLife
  • 7,172
  • 5
  • 55
  • 69
0

One approach not mentioned in any of the other answer is wrapping the operation in a setTimeout. This will also work if you use hooks.

Eg.:

handleChange(input) {
    this.setState({ load: true })
    setTimeout(() => {
        this.props.actions.getItemsFromThirtParty(input).finally(() => {
            this.setState({ load: false })
        });
    });
}

wvdz
  • 16,251
  • 4
  • 53
  • 90