React.useCallback with multiple function arguments, want it uncurried

My reducer function gets called twice for each action, something to do with how React.useCallback is compiled to javascript. I think it has to do with currying. Here is a ridiculously simplified component. The action gets dispatched TWICE the first time the button is clicked but for my real scenario it is dispatched twice on every click.

type action = Increment

let reducer = (s, a) => {
  switch a {
  | Increment => s + 1
  }
}

module SillyComponent = {
  @react.component
  let make = () => {
    let wrapped = React.useCallback1((s, a) => {
      Js.log2("Dispatched an action", a)
      reducer(s, a)
    }, [true])
    let (state, dispatch) = React.useReducer(wrapped, 0)
    <div>
      <button onClick={_ => dispatch(Increment)}> {"click me!"->React.string} </button>
      <div> {state->Int.toString->React.string} </div>
    </div>
  }
}

Here is the generated javascript. Notice the generated javascript is not calling useCallback on a single function. Instead it is memoizing a function that returns a function and that is why my reducer keeps getting called twice. If I take out the nesting and turn it into function (s, a)... it works.

  var wrapped = React.useCallback(
    function (s) {
      return function (a) {
        console.log("Dispatched an action", a);
        return (s + 1) | 0;
      };
    },
    [true]
  );

How can I use React.useCallback to generate a memoized function with multiple arguments that won’t get nested in the javascript AND can be passed to React.useReducer? I tried wrapping the arguments in parenthesis to make it a tuple - let wrapped = React.useCallback1(((s, a)) =>... but then I can’t pass it to useReducer. I tried using the uncurry syntax but couldn’t get it to compile.

I am able to get this all to work using React.useMemo instead. But according to the React documentation it should be possible to make this work using useCallback.

Can you explain why you need to wrap reducer inside a callback? I don’t understand what are you trying to achieve with this approach

Good question. Maybe I’m making it too complicated.

The reducer is defined outside the component. I’m trying to introduce a side effect before the reducer is called so I want to wrap it with another function that performs the side effect. In particular, all my components are controlled components based on the state produced from the reducer. One of those components is a list that generates “selection changed” events. When the state changes, it sometimes updates the list, which causes a “selection changed” event, which can in turn cause an action to be sent to the reducer. So before I call the reducer, I want to set a variable inside the component to “ignore selection changed” events to avoid this looping. After the render completes, I have an effect that sets “ignoreSelectionEvents” to false. Functionally it all worked before I did this but it was tough to get my head around all the cascading events.

Ok so why wrap my reducer in a useMemo or useCallback? A bunch of my other callbacks call the reducer. For example, if the user clicks a button I call the reducer. I thought it was best practice to create those callbacks using useCallback to avoid unnecessary renders. Since those callbacks depend on the reducer, I make the reducer a dependency of those callbacks so if the reducer changes, those callbacks get regenerated. And I don’t want to regenerate the reducer on every render, so I wrap it in a useCallback or useMemo.

I know this sounds like premature optimization, but trying to figure out what is going on using console.log is very tough with a bunch of duplicate calls to the reducer and cascading events.

So I’m left with how to memoize the reducer. I can do it with useMemo, but I’d really like to understand why it can’t be done with useCallback since that it designed for memoizing functions not values.

===

Ugh. Just realized reducers should never have side effects. All I’m trying to do is make something happen before the reducer is invoked. I can make that happen like this. So everywhere I’d ordinarily call dispatch I can call dispatchButFirst.

let (state, dispatch) = React.useReducer(...)
let dispatchButFirst = (a) => {
   // do stuff
   dispatch(a)
}

Don’t need help with this anymore. I have a lot to learn about React. Got it all working with a lot less code and confusion.

1 Like