paul23
paul23

Reputation: 9435

Emulate the second parameter of setState in a useState environment. Let something happen after setState has occured

Well in previous react version we could execute code after the state has changed by doing something like:

setState( prevState => {myval: !prevState.myval}, () => {
    console.log("myval is changed now");
)

And make sure the second "bit" is only executed with the updated code. Now one of the examples describes something "similar" can be achieved using React.useEffect:

const [myval, setMyval] = React.useState(false);
React.useEffect(() => {
    console.log("myval is changed now");
}

//somewhere
setMyval(o => !o);

While this looks fine and dandy, it doesn't help in my case: in my case "what effect needs to happen" depends on the location where the state has changed, originally:

//somewhere
setState( prevState => {myval: !prevState.myval}, () => {
    console.log("myval is changed now");
)
//somewhere else 
setState( prevState => {myval: !prevState.myval}, () => {
    console.log("myval has changed somewhere else");
)

Changing these to useState + setMyval would let the same effect fire at both positions, so we cannot derive in useEffect what action actually needs to occur.

How would I do above in a hook based function component?


A better usecase would be a screen which has two buttons, one to load previous, another to load next. The buttons are disabled while isLoading state is true and only perform actions when loading. To make sure only a single load can happen we cannot just "act" as if state changes immediately: state changing is asynchronous and thus a race condition might occur.

Using a callback upon changing state prevents this race condition and makes sure only a single load happens:

class SomScreen extends React.component {
    state = { isLoading: false }
    render() {
        <div>
            <button 
                disabled={this.state.isLoading} 
                onclick={(e) => {
                    if (!this.state.isLoading) {
                            this.setState((p) => ({isLoading: true}), () => {
                            await fetchPreviousDataAndDoSomethingWithIt()
                            setState({isLoading: false});
                        });
                    }
                }
            />Previous</button>
            <button 
                disabled={this.state.isLoading} 
                onclick={(e) => {
                    if (!this.state.isLoading) {
                        this.setState((p) => ({isLoading: true}), () => {
                            await fetchNextDataAndDoSomethingWithIt()
                            setState({isLoading: false});
                        });
                    }
                }
            />Next</button>
        </div>
    }
}

Upvotes: 5

Views: 1215

Answers (1)

Ross Allen
Ross Allen

Reputation: 44880

The question "is an async action in flight" is a reduce across "are any of these async actions in flight", so you could model each async action separately and then compute isLoading. A custom hook like this might work:

function useAsyncAction(asyncAction) {
  const [isLoading, setIsLoading] = useState(false);
  const callAsyncAction = useCallback(async () => {
    setIsLoading(true);
    await asyncAction();
    setIsLoading(false);
  }, []);
  return [
    isLoading,
    callAsyncAction,
  ]
}

//

function SomScreen() {
  const [asyncAIsLoading, callAsyncA] =
    useAsyncAction(fetchPreviousDataAndDoSomethingWithIt);
  const [asyncBIsLoading, callAsyncB] =
    useAsyncAction(fetchNextDataAndDoSomethingWithIt);
  const isLoading = asyncAIsLoading || asyncBIsLoading

  ...
}

Upvotes: 1

Related Questions