immer icon indicating copy to clipboard operation
immer copied to clipboard

How to type a custom produce function?

Open alexturpin opened this issue 3 years ago • 1 comments

🙋‍♂ Question

Hey folks. I'm using Immer with React and I want to create a custom produce function that does something with the new state. I want to pass this produce function down to components so they can call it to update the state, and so that I can do something with the newly updated state. I can't seem to figure out how to type that function however. It seems that ValidRecipeReturnType isn't exported. I was able to do it with use-immer's DraftFunction but use-immer doesn't return the new state after it's set.

Link to repro

https://codesandbox.io/s/festive-browser-v2wj8?file=/src/App.tsx

How can I type the recipe param from updateState?

Environment

Latest React and Immer.

Thank you!

alexturpin avatar Jan 20 '22 03:01 alexturpin

I use an approach like this in my applications:

type Recipe = (arg0: Draft<MyState>) => void | MyState;

RTK-Query also annotates the 'recipe' approach: https://github.com/reduxjs/redux-toolkit/blob/2425f02fe68f7e186c21e009605022013778a2c5/packages/toolkit/src/query/core/buildThunks.ts#L156

Here is a fork of your sandbox that has working annotation:

import produce from "immer";
import type { Draft } from "immer";
import { useCallback, useState } from "react";
import "./styles.css";

type MyState = {
  foo: string,
  bar: string
};

const initialState = {
  foo: "",
  bar: "",
}

type Recipe = (arg0: Draft<MyState>) => void | MyState;

export default function App() {
  const [state, setState] = useState<MyState>(initialState);

  const updateState = useCallback((recipe: Recipe) => {
    setState((previousState) => produce(previousState, recipe));
  }, [setState]);

  const recipeCallback: Recipe = (state) => {
    state.foo = "Muffin Man";
  };


  return (
    <div className="App">
      <h1>Hello {state?.foo}</h1>
      <button
        onClick={() => {
          updateState(cb);
        }}
        type="button"
      >
        click
      </button>
    </div>
  );
}

Unrelated, but check the React docs on useState here: https://reactjs.org/docs/hooks-reference.html#functional-updates

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

This is important because the state update can now run outside of the render context/timing. I'm not a react render wizard, so I can't tell you how this affects batching or render timing, but it's good to use the callback notation when computing the next state from the previous. Good luck!

bever1337 avatar Feb 26 '23 09:02 bever1337