immerjs / use-immer

Use immer to drive state with a React hooks
MIT License
4.04k stars 92 forks source link

call after immer state update not using next state #66

Closed PHILLIPS71 closed 4 years ago

PHILLIPS71 commented 4 years ago

A bit stumped here if this is a bug or I'm doing something wrong. I'm trying to push a new item into an array, then update the db based on what is within the state. The view renders the state correctly with next state but the function call is still using the old state.

Unsure if these producers are async in some form, but according to the Typescript typings they aren't, so I cannot await the state update before the db call.

Pretty much trying to update the state, then execute the register mutation. The user state still has an empty array when the mutation is called, but the view renders it correctly, so the state does eventually get updated.

<SecurityStep
  onComplete={(data) => {
    setUser((draft) => {
      draft.credentials.create.push({
        type: CredentialType.Basic,
        token: data.password,
      });
    });

    register({ variables: { data: user } }).then(() =>
      setStep(ProgressionStep.COMPlETE)
    );
  }}
/>

immer state initialization

  const [user, setUser] = useImmer<UserCreateInput>({
    first_name: null,
    last_name: null,
    email: null,
    avatar: null,
    credentials: {
      create: [],
    },
  })
mweststrate commented 4 years ago

Please provide a minimal reproduction in code sandbox for example.

PHILLIPS71 commented 4 years ago

I've create a code standbox with extracts from the project I noticed this in. The user state is always behind when calling the console log directly after the setUser. I'd expect this to have the most recent state that was just set above the log, but correct me if I'm doing something wrong.

https://codesandbox.io/s/objective-grothendieck-x99pb?file=/pages/index.tsx

mweststrate commented 4 years ago

@PHILLIPS71 the behavior you observe is correct, useImmer uses useState under the hoot, and useState isn't synchronous nor will the changes be immediately visible, but only in the next render. Remember that state updates should be side effect free for that reason. There are two ways you can work around this:

  1. use useEffect; useEffect(() => { console.log(user) }, [user]). This is the idiomatic thing to do
  2. call produce manually and use useState directly. E.g. const [user, setUser] = useState(stuff); .... setUser(user => { const newUser = produce(user, draft => { ... }); console.log(newUser); return newUser; })
PHILLIPS71 commented 4 years ago

@mweststrate thanks for your response it was very helpful, I've gone ahead and used useEffect as this make much more sense.

rodrimaia commented 2 years ago

Thank you for the answer, @mweststrate . I would like to point that this example on README.md missleads us to think that the changes will be immediately visible (since age is being called on the alert message just after the setAge was called). image