immerjs / use-immer

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

Add helper to wrap state from other hooks? #57

Closed ianstormtaylor closed 3 years ago

ianstormtaylor commented 4 years ago

Running into a situation that I think would be nicely handled in use-immer since it seems somewhat common...

If you a value is scoped to your component, then the current useImmer works well. But if you don't have control over the value (eg. it's passed from a parent, or it's gotten from local storage, etc.) then you have the deal with the annoyance of using a callback to set the new state.

But it would be nice to be able to do something like:

function Example() {
  // The "user" lives in Local Storage and should be updated on changes.
  // This returns `[user, setUser]`, but you want to use immer to update it.
  const userState = useLocalStorage('user')

  // So you can wrap the state tuple easily and have this new hook handle 
  // the callback logic for you to save.
  const [user, updateUser] = useImmerState(userState)

  // Here the `updateValue` knows to call `setValue` under the covers 
  // to save the update in Local Storage.
  updateUser(user => {
    user.name = 'New Name'
  })
}

Since the [value, setValue] pattern is widespread, this new hook would be very flexible and could be used to augment any existing tuple-returning state hook.

What do you think?

ianstormtaylor commented 4 years ago

An alternative way to handle this nicely would be to allow useImmer to take a second argument that is a callback called whenever the update happens. Which would allow you to write the same example as:

function Example() {
  const [user, setUser] = useLocalStorage('user')
  const [, updateUser] = useImmer(user, setUser)

  updateUser(user => {
    user.name = 'New Name'
  })
}

I think this is actually more flexible, so this would be the better solution.

akshayjai1 commented 4 years ago

Hi Ian,

Can you please update the Example component, I am not able to figure out what this component intends to do, and that will also help get more clarity about your issue.

Thank you

mweststrate commented 4 years ago

@ianstormtaylor I think I get what you are getting at. I think the first solution is nicer, the second one looks incorrect, as immer wouldn't receive the new user after an update.

However, I think this is a simpler solution that doesn't need new api's:

import {produce} from immer

function Example() {
  const [user, _setUser] = useLocalStorage('user')
  const updateUser = useMemo(() => produce(_setUser), [])

  updateUser(user => {
    user.name = 'New Name'
  })
}
Newbie012 commented 3 years ago

@mweststrate How would you implement such a thing using reducer?

Edit - I have managed to do so with useEffect:

export function useCart() {
  const [storageCart, setStorageCart] = useLocalStorage("cart", initalCart);
  const [cart, dispatch] = useImmerReducer<Cart>(reducer, storageCart);

  React.useEffect(() => {
    setStorageCart(cart);
  }, [cart, setStorageCart]);

  return [cart, dispatch] as const;
}