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

[hooks] useState - "setState" callback #98

Closed
marcellomontemagno opened this issue Dec 19, 2018 · 36 comments
Closed

[hooks] useState - "setState" callback #98

marcellomontemagno opened this issue Dec 19, 2018 · 36 comments

Comments

@marcellomontemagno
Copy link

marcellomontemagno commented Dec 19, 2018

Hi, I'm very hyped about the hooks api and I started experimenting with it on some small side project.

I've noticed that the "setState" function returned by the useState hook doesn't have an "afterRender" callback (the second argument of this.setState in classes)

I didn't find further informations about this in the doc. Is there any design reason why the "afterRender" callback is not present? Is there a plan to introduce it later on?

The reason I ask is that during the past years of react usage I found a few scenarios (very rarely though) were not having the "afterRender" callback would result in a quite tedious implementation of componentDidUpdate or pushed me to change the shape of my state to work around it (I can provide examples about this)

In the meanwhile here the snippet I used to understand that no "afterRender" callback is used by the "setState" of hooks

Counter.js

import React, {useState} from 'react';

export const defaultState = {count: 0}

export const useCounter = () => {

  const [state, setState] = useState(defaultState)

  return {
    ...state,
    onIncreaseCounter () {
      setState({...state, count: state.count + 1}, () => {console.log('++++++++++++++++++')})
    },
    onDecreaseCounter () {
      setState({...state, count: state.count - 1}, () => {console.log('++++++++++++++++++')})
    }
  }

}

export default function Counter ({count, onIncreaseCounter, onDecreaseCounter}) {
  return <div>
    {count}
    <button onClick={onIncreaseCounter}>+</button>
    <button onClick={onDecreaseCounter}>-</button>
  </div>
}

Root.js

import React from 'react';

import Counter, {useCounter} from "./Counter";

export default function Root () {
  return <Counter {...useCounter()}/>
}

the log never appears in the console after after increasing and decreasing the counter

@jquense
Copy link

jquense commented Dec 19, 2018

@sandiiarov that won't do what @marcellomontemagno is asking, it just logs immediately, not in the callback.

@marcellomontemagno you can use a useEffect hook to do 'after render' callbacks and coordinate with the current state value

@gustavo-depaula
Copy link

Yes, there is not a second argument on the setState hook callback. +1'd because I also want to know the reason why.

@renatoagds
Copy link

renatoagds commented Jan 7, 2019

@marcellomontemagno agree with @jquense to use useEffect, since setState will cause a re-render and it will call useEffect.

@heyimalex
Copy link

Could someone write an example? Imagine a list of items that a user can cursor through. Whenever we get a keydown event on an arrow we update the cursor position, and then in the callback we scroll the currently selected element into view.

const itemCount = 10;
const [cursor, setCursor] = useState(0);
const handleKeyDown = e => {
  if (e.key === 'ArrowUp') {
    setCursor(cursor => Math.max(0, cursor - 1), () => {
      // scroll element into view
    });
  } else if (e.key === 'ArrowDown') {
    setCursor(cursor => Math.min(itemCount - 1, cursor + 1), , () => {
      // scroll element into view
    });
  }
}

Lets also say that arrow keys aren't the only reason that cursor changes, but are the only reason we want to scroll. It seems like we'd need to track some extra state to make this work with useEffect.

const itemCount = 10;
const [state, setState] = useState({ cursor: 0, scrollKey: 0 });
const handleKeyDown = e => {
  if (e.key === "ArrowUp") {
    setState(state => ({
      cursor: Math.max(0, state.cursor - 1),
      scrollKey: state.scrollKey + 1
    }));
  } else if (e.key === "ArrowDown") {
    setState(state => ({
      cursor: Math.min(itemCount - 1, state.cursor + 1),
      scrollKey: state.scrollKey + 1
    }));
  }
};
useEffect(
  () => {
    if (state.scrollKey > 0) {
      // scroll to current element
    }
  },
  [state.scrollKey]
);

Does that look about right?

@MohamedLamineAllal
Copy link

MohamedLamineAllal commented Apr 2, 2019

Same here, i find the callback very useful. Too i can't imaging a scenario why this should be removed. Or how it influence performance. I really want to know the why ! For me i just though it's a one call or a resolve of a promise. That will happen when the updates happen. I don't know but it's seem that there is more to it. facebook/react#14174 (comment) (this thread is treating that better). So i guess i've got to look at how the hooc model is implemented and work.

However combining with useEffect. we can use a promise, that we resolve in useEffect. And keep more of the flexibility. Even thogh not too cool.

Check this (a more lively thread, with a lot of context) #68 (comment)

@phryneas
Copy link

phryneas commented Apr 2, 2019

Because it invokes a thought pattern that may very well be an antipattern with hooks.

With hooks, you are writing code more declarative than imparative.

The thought of "do one thing THEN do another thing THEN do another thing" would be the imparative (here: callback- or promise-based) way.

What actually should be done in the hooks model would rather be a "if this has been rendered, and this property is different than before, X should be done to reach the target state", which is more declarative. You don't designate when stuff happens, but just give a condition/dependency and let react pick the right point in time for it.

@jamesplease
Copy link

jamesplease commented Apr 3, 2019

Folks may already know all of/some of this, but I figured I'd post it just in case. The warning that's logged when trying to use the 2nd callback argument provides a hint as to why it isn't supported:

image

Judging from the message, it seems that using useEffect is preferred instead, which is an alternative @jquense mentioned above .

Here's a CodeSandbox example of the Counter from the initial post using useEffect: CodeSandbox link

And also just to clarify again, I know you may already know all of this @marcellomontemagno but since hooks are new-ish I figured I'd post this just in case! 😅

@alex35mil
Copy link

alex35mil commented Apr 13, 2019

What actually should be done in the hooks model would rather be a "if this has been rendered, and this property is different than before, X should be done to reach the target state", which is more declarative. You don't designate when stuff happens, but just give a condition/dependency and let react pick the right point in time for it.

This reminds me of a game "guess what to do in componentWillReceiveProps by comparing N different things".


I have a form library and I'm thinking of how to update it to the new API. I'm just getting myself familiar with hooks and got stuck pretty soon on the following case: user submits a form.

In the previous class-based model, when Submit action is dispatched, I update a state and trigger onSubmit effect, using setState callback (UpdateWithSideEffects callback in Reason). Simple as that.

In the hooks world I can't do that. I have a reducer that returns a state only and useEffect that runs (or not) on each re-render independently. Schedule of useEffect can be managed by specifying array of deps and if one of the deps has changed, effect is triggered. So, if I understand docs correctly, I need to:

  1. find a value in state that is changed on Submit and pass it to deps array: this is a form status
  2. inside effect, check if current value of form status is Submitting then trigger onSubmit handler

Here's the first confusing point: what if re-render was triggered during submission for some unrelated reason (e.g. parent props was updated due to whatever). In fact, submission request was triggered and is ongoing but since re-render was triggered and form status is still Submitting -> all conditions are met to re-run effect.

This common case seems pretty tricky with hooks. But hopefully I just didn't grok it yet and there is clean way to handle it within the new model.

@phryneas
Copy link

phryneas commented Apr 13, 2019

Usually, I'd trigger the asynchronous side-effect directly from button.onClick or form.onSubmit and store the results back using a useReducer dispatch. Since that seems not to be an option for you, maybe do something along these lines?

depend only on isSubmitting, pass state via Ref

const [state, dispatch] = useReducer(...);
const stateRef = useRef();
stateRef.current = state;

useEffect(() => {
  if (!state.isSubmitting) { return; }
  // do your submitting with stateRef.current in here
  // then dispatch the results
}, [state. isSubmitting]);

The effect only triggers when state.isSubmitting changes and within it, you have access to the latest state using the ref. Though I would not recommend using this reference hack in every situation, in your case it seems fitting.

But keep in mind: if you have a promise chain in your useEffect and access the stateRef later (with a few renders in between), you might access a later version of the state, not the state at the time the effect was triggered

Or, you omit the reference here, that way you would have access to the state at the time of triggering the effect - but you would have eslint-plugin-react-hooks linter warning that you might need to ignore.

depend on full state, keep track of last submit in a Ref

in this case, we assume a property state.submitCount that is increased by 1 with every submit

const [state, dispatch] = useReducer(...);
const lastSubmittedRef = useRef(0);

useEffect(() => {
  if (lastSubmittedRef.current === state.submitCount) { return; }
  lastSubmittedRef.current = state.submitCount;
  // handling here
}, [state]);

@alex35mil
Copy link

I spent some time today on this and ended up with using custom hook that brings back UpdateWithSideEffects to hooks with almost zero changes to initial reducer: https://github.com/alexfedoseev/re-formality/pull/51/files#diff-376207e44be3824884705dd487fd098c

Looks like it works. All props to @bloodyowl for initial gist: https://gist.github.com/bloodyowl/64861aaf1f53cfe0eb340c3ea2250b47

@rwieruch
Copy link

Yes, you can use useEffect/useLayoutEffect to add your custom callback function after updating state. If you are looking for an out of the box solution, maybe this custom hook which is nothing else than useState with a callback function may be handy for you.

@slorber
Copy link

slorber commented Jun 11, 2019

@rwieruch your code is dangerous, everytime the callback changes (which does happen frequently if using closures) the callback will fire again. You need to "stabilize" your callbacks using refs (Dan Abramov has a nice blog post somewhere on that topic)

@kdzhambazov
Copy link

kdzhambazov commented Jun 20, 2019

I understand that from a general case point of view useEffect() will run our code on the place of the old setState() callback, however consider this case:

...
onChange() {
...
this.setState(stateToSet, () => {
if (this.props.parentOnChange) {
this.props.parentOnChange(this.state);
}
});
....
}

onBlur() {
...
this.setState(stateToSet, () => {
if (this.props.parentOnBlur) {
this.props.parentOnBlur(this.state);
}
});
....
}

So here we want to execute 2 different callbacks depending on the event. If we fire onChange() we want to fire its parent's onChange if provided in the component props and so on. In short every type of setState() needs to have a separate useEffect(). How can this be managed with hooks without the "removed" callback. The only way I could think of, is to have additional if / else logic inside the useEffect() hook ?

@phryneas
Copy link

Exactly that example is why you wouldn't want that callback.
In your case, usually you want to directly call props.parentOnChange after calling setState, without waiting for setState to acutally happen. (so you wouldn't use useEffect as well!)
The reasoning for that is pretty simple: if props.parentOnChange also triggers a state change, those two will be batched together . But if you wait until after the first setState has completed, you might introduce a double-render and inbetween have inconsistent states.
Also, I can't think of many situations where you really would want to wait for setState to propagate before calling that parent callback - the parent has no access to the internal state of the child component anyways, so there'd be no way the child state is of any interest to the parent component.

@rwieruch
Copy link

Also, I can't think of many situations where you really would want to wait for setState to propagate before calling that parent callback ...

In my case, I wanted to wait for the re-render, because afterward I can operate on a ref which would be hidden before due a conditional rendering. For instance, I have a boolean state which either shows a list or an item to edit. If I am in the latter state, I want to save the edited item and return to the list of items. As soon as the re-render completes, and all list items with their refs are available, I can scroll programmatically to the updated item which has been edited.

@eslachance
Copy link

The need for a way to "chain" setState is one that I find myself having to deal with. Consider the following code: https://text.evie.codes/igacuvadug.js . Here, my issue is that I need to do this on clicking my component :

  • set the Spring reset to true
  • after it's set to true, reset my percentage to 0, so it's "instantaneous" and not animated
  • Once that's done, increment my count, then set the Spring reset to false, and then set my component to no longer be clickable.

With the lack of callback (which, to be fair, I'd immediately turn into a promise and use async/await, because c'mon, callback hell!), I find myself unable to chain these in any meaninful fashion. Since those operations become batched, it means my progress bar's position resets to zero without the Spring reset being triggered, so it doesn't just go to 0, it moves to 0.

The suggestions above (and what I've seen previously in terms of useEffect solutions) account for replacing one callback but I need to replace 4 of them (I could batch some of these things, but not all of them). The useEffect condition hell I would put myself in sounds like it would be a nightmare to manage.

@phryneas
Copy link

@eslachance here's a working example without useEffect at all: https://codesandbox.io/s/weathered-tdd-794hs

Here's what I did:

  • useReducer instead of five calls to state setters so all state is set in one atomic action
  • keep track of last progress - reset is true when lastProgress > progress, no need to track any "reset" thing itself.
  • reset belongs on Spring, not on ProgressWrapper

@eslachance
Copy link

Damn that's why my reset wasn't working 😅
Thank you @phryneas , that's going above and beyond to provide a working example. I will hold on to it preciously. You're awesome!

@slorber
Copy link

slorber commented Jul 1, 2019

Hey all

This is how I solve it in my apps. Just published it as an opensource package:

https://github.com/slorber/use-async-setState

import { useAsyncSetState, useGetState } from "use-async-setState";

const Comp = () => {
  const [state,setStateAsync] = useAsyncSetState({ counter: 0 });
  const getState = useGetState(state);
  
  const incrementTwiceAndSubmit = async () => {
    await setStateAsync({...getState(): counter: getState().counter + 1});
    await setStateAsync({...getState(): counter: getState().counter + 1});
    await submitState(getState());
  }
  
  return <div>...</div> 
}   

@arjun-menon
Copy link

Great thread! I was wondering about the same thing.

@yairEO
Copy link

yairEO commented Aug 26, 2019

There's a very good discussion on Stackoverflow:
Equivalent to componentDidUpdate using React hooks

The useEffect method for tracking useState updates is (in some cases) undesirable, since it is invoked immediately on mount, regardless of a state change.


My scenario is that I have some onChnage callback prop passed to my <Select> component which must only be fired when a change was actually made, because it triggers a heavy server request.

@rwieruch
Copy link

There's a very good discussion on Stackoverflow:
Equivalent to componentDidUpdate using React hooks

The useEffect method for tracking useState updates is (in some cases) undesirable, since it is invoked immediately on mount, regardless of a state change.

My scenario is that I have some onChnage callback prop passed to my component which, which *must only be fired when a change was actually made, because it triggers a heavy server request.

Do you want to open a PR for https://github.com/the-road-to-learn-react/use-state-with-callback to include the prevention of calling the callback on mount? Didn't think about it for my use case back then, but I think it should be only something along these lines:

const isMounted = React.useRef(false);

React.useEffect(() => {
  if (isMounted.current) {
    return;
  }

  // do callback

  isMounted.current = true;
});

@iremlaya
Copy link

+1

@sag1v
Copy link

sag1v commented Oct 2, 2019

There is another scenario which one might want a to run a callback per state update, i think its the same or similar to what was mentioned in this comment by @kdzhambazov .

If you want to run 2 different callbacks for the same data that was changed, you can't do that only with useEffect. This will require some extra logic, maybe tracking a callback queue, some state to determine if this is the first render and if an update was triggered but the state didn't really changed because the next value is the same as previous (in classes the callback will trigger even if the state wasn't really updated).

This can be a custom hook obviously:

function useStateWithCallback(initialValue) {
  const [state, setState] = useState(initialValue);

  // we need this flag for 2 reasons:
  // 1. To prevent the call on mount (first useEffect call)
  // 2. To force the effect to run when the state wasn't really updated
  // i.e next-state === previous-state.
  const [shouldRunCBs, setRunCBs] = useState(false);

  // tracking a queue because we may have more than 1 callback per update?
  const cbQRef = useRef([]);

  function customSetState(value, cb) {
    if (typeof cb === "function") {
      cbQRef.current.push(cb);
      // we force the effect to run even if the state wasn't really updated
      // i.e next-state === previous-state.
      // this is how the callback in classes work as well
      // we can opt-out from this behaviour though
      setRunCBs(true);
    }
    setState(value);
  }

  useEffect(() => {
    if (shouldRunCBs) {
      // we must pass back the new value
      //because the consumers can't get it via the closure of thier component
      // and they don't have an instance like in classes.
      cbQRef.current.forEach(cb => cb(state));
      cbQRef.current = [];
      setRunCBs(false);
    }
  }, [state, shouldRunCBs]);

  return [state, useCallback(customSetState, [])];
}

Here is a sandbox with a simple use case

@Jessidhia
Copy link

Jessidhia commented Oct 2, 2019

That almost works -- if using synchronous React (ReactDOM.render) and the state change comes from outside React (e.g. something that runs after an await, or other non-React events), you will get the callback invoked with the old value, not the new one, because React will update immediately with setRunCBs(true), the Effect will be prepared, and it will be invoked when you try to call setState(value). Then when the new value is entered it won't have callbacks to call anymore.


This very specific nitpicking aside (which you can't reproduce in your sandbox as you're using React events), this pattern still looks really strange. If you want to guarantee a setState always causes an update, then set it to an object, as objects will never be Object.is another object. Use one Effect for each side effect you want to trigger. Use a Ref to conditionally enable the effect before dispatching if you want it to be very specific to where you're dispatching from (which should be a code smell to begin with).

@sag1v
Copy link

sag1v commented Oct 2, 2019

@Jessidhia Thanks for the input, can you create a sandbox with your async example and my hook showing the bug?

Edit
OK i think i understand what you mean, just tried to register via addEventListener to force sync update and i see the issue.

I managed to bypass this limitation by tracking last value in a ref. Not sure this is considered as a good approach, but this is a valid use case for a callback and there is no native way of achieving it. 🤷‍♂

function useStateWithCallback(initialValue) {
  const [state, setState] = useState(initialValue);

  // we need to track down last changes to support synchronous updates
  // e.g, addEventListener handlers
  const lastStateRef = useRef(initialValue);

  // we need this flag for 2 reasons:
  // 1. To prevent the call on mount (first useEffect call)
  // 2. To force the effect to run when the state wasn't really updated
  // i.e next-state === previous-state.
  const [shouldRunCBs, setRunCBs] = useState(false);

  // tracking a queue because we may have more than 1 callback per update?
  const cbQRef = useRef([]);

  function customSetState(value, cb) {
    if (typeof cb === "function") {
      cbQRef.current.push(cb);
      // we force the effect to run even if the state wasn't really updated
      // i.e next-state === previous-state.
      // this is how the callback in classes work as well
      // we can opt-out from this behaviour though
      setRunCBs(true);
    }
    setState(value);
  }

  useEffect(() => {
    if (shouldRunCBs && state !== lastStateRef.current) {
      // we must pass back the new value
      //because the consumers can't get it via the closure of thier component
      // and they don't have an instance like in classes.
      cbQRef.current.forEach(cb => cb(state));
      cbQRef.current = [];
      setRunCBs(false);
      lastStateRef.current = state;
    }
  }, [state, shouldRunCBs]);

  return [state, useCallback(customSetState, [])];
}

@chenyanfei-m
Copy link

I use it like this:

import React, { useState, useRef, useEffect } from 'react'

export default function useStateWithCallback(initState) {
    const callbackRef = useRef(null)

    const [state, setState] = useState(initState)

    useEffect(() => {
        if (callbackRef.current) {
            callbackRef.current(state)
            callbackRef.current = null
        }
    }, [state])

    const setCallbackState = (value, callback) => {
        callbackRef.current = typeof callback === 'function' ? callback : null
        setState(value)
    }

    return [state, setCallbackState]
}
const [index, setIndex] = useStateWithCallback(0)
setIndex(1, () => console.log('callback'))

@slorber
Copy link

slorber commented Nov 21, 2019

Most solutions here have flaws:

  • using a boolean state and producing useless re-renders
  • using a single callback ref won't work if multiple consecutive setState calls are performed
  • doing setState(1,cb) if state is already 1 won't trigger a re-render so your callback won't be triggered if it's only handled in useEffect
  • also wonder why do you really prefer a function callback compared to a promise, isn't it simpler to deal with promises to build complex flows?

Take another look at https://github.com/slorber/use-async-setState please:

  • I've handled the edge cases above
  • It has full TS support, for both setStateAsync(obj) and setStateAsync(fn)
  • It has a promise-based interface (ok to support callbacks too if you want to open a PR)
  • I run this in production for months
  • It has tests

image

@slorber
Copy link

slorber commented Nov 21, 2019

@sag1v your solution is probably the closest to mine except doing setState(1,cb) if state=1 will lead to an extra unnecessary re-render. Just look how I handled this more efficiently here: https://github.com/slorber/use-async-setState/blob/master/src/index.ts#L33

@sag1v
Copy link

sag1v commented Nov 21, 2019

@slorber Thanks. It was just a naive implementation example. My point was to show there are use cases for a state update callback, it is doable in user-land but maybe it should be provided by the library as it is with classes.

@arkakkar
Copy link

arkakkar commented Apr 8, 2020

useEffect is the proper solution to consume the updated state, but useEffect is called everytime the state is updated, so I wouldn't want to call my setState callback everytime. Can return a promise instead and use the thenable callback on-demand.
Here is a simple solution to the problem:

updateState({...newState}).catch(err => logger.log(err)).then(updatedState => {
      //do whatever you want with updated state
})

const promiseRef = useRef({});
updateState = (newState) => {
    promiseRef.current.promise = new Promise((resolve, reject) => {
          promiseRef.current.isPending = true;
          promiseRef.current.resolve = resolve;

     /** update state here through **useState** or **useReducer dispatch** with newState */

    });
    
    return promiseRef.current.promise;
}

/** deep object comparison of state */
useEffect(() => {
        let {promise, isPending, resolve} = promiseRef.current;
        if (promise && isPending && typeof resolve == 'function') {
            promiseRef.current.isPending = false;
            promiseRef.current.resolve(state);
            promiseRef.current.resolve = null;
        }
},[state]);

@slorber
Copy link

slorber commented Apr 8, 2020

@arkakkar this is what I do with https://github.com/slorber/use-async-setState

You also need to handle case like await setState(s => s) (which does not trigger useEffect) + handle rejects etc

@adamkleingit
Copy link

Also, I can't think of many situations where you really would want to wait for setState to propagate before calling that parent callback ...

In my case, I wanted to wait for the re-render, because afterward I can operate on a ref which would be hidden before due a conditional rendering. For instance, I have a boolean state which either shows a list or an item to edit. If I am in the latter state, I want to save the edited item and return to the list of items. As soon as the re-render completes, and all list items with their refs are available, I can scroll programmatically to the updated item which has been edited.

Then you should have a useEffect on the boolean flag and scroll to the list if it changes. Something like:

useEffect(() => {
  if (!isEditing) {
    listRef.current.scrollTo();
  }
}, [isEditing]);

Because useEffect runs after rendering, the ref will be available

@Genarito
Copy link

Genarito commented Apr 22, 2020

I don't understand why people are reluctant to add a second parameter to setState (or other Hook). Hooks were made to make React simpler... But instead of writting:

this.setState({ loading: true}, () => {
    fetch('...').then(() => this.setState({loading: false}))
    .catch(() => this.setState({loading: false}))
})

Which is much more clearer and compact you propose to make a useEffect which checks if the state has changed to set a simple flag. I have a lot of requests with their own flags, and instead of making things simpler the proposal is write 423K of useEffect functions in the same file to handle every one.
This is a very requested feature, in fact, most of the custom Hook lists you can find on Github include a Hook with callback. It'd be cool to have a built in solution

@marcellomontemagno
Copy link
Author

marcellomontemagno commented May 16, 2020

I think I'm happy with this reply #98 (comment)

This is still something people will have to deal with so here a recipe to deal with the lack of setState callback (the recipe should be able to wrap up all the feedback recieved in this discussion so far):

Option 1: subscribe to the state change you made via useCallback

This option might not be suitable for you if that very same state change can happen for many different reasons.

Options 2: in the event callback where the first setState is happening, just invoke another setState right after the first one

This works because, even though the first setState is indeed asynchronous, the order of the 2 setState is still guaranteed.

Option 2 might not be suitable for you if instead of invoking a second setState you need to invoke an imperative function of the browser such as inputRef.focus() as the browser will not wait for the first setState to be applied to the DOM.

If you need to invoke an imperative API from the browser such as inputRef.focus() and option 1 is not suitable for your use case you have the following 2 options available:

Option 2.1: hide the imperative API from the browser making react your only source of truth.

Here an example of how to try to convert an imperative API from the browser in something react can control:

import React, {useRef, useEffect, useState} from 'react'

const EnhancedInput = ({focus, onFocus, onBlur, ...rest}) => {

  const inputRef = useRef(false)

  useEffect(() => {
    focus && inputRef.current.focus()
  }, [focus])

  return <input
    ref={inputRef}
    onBlur={onBlur}
    onFocus={onFocus}
    {...rest}
  />

}


const ExampleUsage = () => {

  const [hasFocus, setHasFocus] = useState(false)

  return <EnhancedInput focus={hasFocus} onFocus={() => setHasFocus(true)} onBlur={() => setHasFocus(false)}/>

}

It might be desirable to have React as the single source of truth within your app and after doing so you can then fall back on option 2 invoking setHasFocus right after your first setState.

Options 2.2: reintroduce the setState callback by yourself.

@slorber did a good job in making this something reusable via https://github.com/slorber/use-async-setState.


To wrap this up, my personal opinion is that is good to think about option 1 first but option 2 might be desirable more often.

Please feel free to follow up with any concern about this recipe or just leave a 👍 If you agree.

if confirmed, I would be happy to formalize this better and add a FAQ to the react documentation about hooks at some point as this discussion had quite a lot of replies.

@gaearon
Copy link
Member

gaearon commented Aug 24, 2021

Hi, thanks for your suggestion. RFCs should be submitted as pull requests, not issues. I will close this issue but feel free to resubmit in the PR format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests