How to Pass Value to useCallback in React Hooks

UseCallback allows having the same reference for a call back function which has the benefit of only changing when something changes. This is primordial when callbacks are passed down the React component chain to avoid component to be rendered without an actual real reason causing performance issue. The following snippet of code shows a simple button that when clicked invokes an action that set the name to “test”. You can imagine that in a real scenario that the string would come from a real source instead of being hardcoded.

<button
  onClick={() => {
    dispatch(AppActions.setName("test"));
  }}
>

The action can often be handled without passing data, or by passing a React’s property and hence can access it from the handler of the action. However, in some cases, where the value is not accessible directly from the outer scope of the handler function, it means that we need to pass by parameter the value. I am reusing a Code Sandbox slightly modified to have the useCallback with a value passed down. The use of useCallback or simply the refactoring of the above snippet into a function that is not directly bound to the onClick is similar. We are moving the accessible scope. When the function is inline, the function can access anything that defined the button. It can be the React’s properties, or the “map” index if it was inside a loop or else. However, extracting the function out require some minor change to still have access to the value.

 const setName = useCallback(() => {
    dispatch(AppActions.setName("test"));
 }, []);

A quick change with React Hooks to produce the desired scenario is to use useCallback at the top of the component and access it directly in the onClick function callback.

<button onClick={setName}>Change name</button>

At the moment, it works. However, we are not passing any information. Let’s imagine that we cannot access the data directly from the useCallback, how can we still invoke this one?

const setName = useCallback((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    dispatch(AppActions.setName("test"));
  }, []);

The idea is to have a callback that return a function that as on its turn the input event.

<button onClick={setName("Testing Name")}>Change name</button>

The invocation code change by passing the data. In that example, it is a string, but you can imagine that you are passing the index of the map function or data coming from a source inaccessible from the callback function.

  const setName = useCallback(
    (name: string) => (
      event: React.MouseEvent<HTMLButtonElement, MouseEvent>
    ) => {
      dispatch(AppActions.setName(name));
    },
    []
  );

My rule of thumb is that I do not need to have this convoluted definition if I am accessing directly properties of the component. Otherwise, I am passing the data needed. I always define the type, which gives me a good quick view about what is passed (name is a string and the event is a mouse event) without having to rummage the code. Here is the code sand box to play with the code of this article.

TypeScript with Strong Typed Action when using useReducer of React Hooks

The technic I will demonstrate in this article comes from what I was using with the traditional React and Redux. Now, with React Hooks we do not need to use Redux anymore but the same principles exist with the hook useReducer. I have witnessed many people struggling using TypeScript and Redux because they are passing strings around instead of a string literal. Often, the struggle I hear is around the boilerplate required as well as it is not strongly typed. Let’s try to make it as simple as possible and strongly typed in term of action name and action payload

Code

If you want to play around with what I will present, you can jump into the code sandbox available under this paragraph. However, keep in mind that you will not see the benefit of having Intellisense showing available actions and the security of working in VsCode where it will warn you if you are passing something of the wrong type — this is a limitation of the online sandbox tool. The demonstration has many files, I suggest you click “Open in Editor” and click the hamburger menu to navigate between files.

Configuring the Actions

The first step is to get some types. Once you have that code in your project, you will be good to go without touching it. I am posting the code and will explain what it does.

/**
 * Create an action that has a strongly typed string literal name with a strongly typed payload
 */
export function createActionPayload<TypeAction, TypePayload>(
  actionType: TypeAction
): (payload: TypePayload) => ActionsWithPayload<TypeAction, TypePayload> {
  return (p: TypePayload): ActionsWithPayload<TypeAction, TypePayload> => {
    return {
      payload: p,
      type: actionType
    };
  };
}

/**
 * Create an action with no payload
 */
export function createAction<TypeAction>(
  actionType: TypeAction
): () => ActionsWithoutPayload<TypeAction> {
  return (): ActionsWithoutPayload<TypeAction> => {
    return {
      type: actionType
    };
  };
}
/**
 * Create an action with a payload
 */
export interface ActionsWithPayload<TypeAction, TypePayload> {
  type: TypeAction;
  payload: TypePayload;
}

/**
 * Create an action that does not have a payload
 */
export interface ActionsWithoutPayload<TypeAction> {
  type: TypeAction;
}

/**
 * A very general type that means to be "an object with a many field created with createActionPayload and createAction
 */
interface ActionCreatorsMapObject {
  [key: string]: (
    ...args: any[]
  ) => ActionsWithPayload<any, any> | ActionsWithoutPayload<any>;
}

/**
 * Use this Type to merge several action object that has field created with createActionPayload or createAction
 * E.g. type ReducerWithActionFromTwoObjects = ActionsUnion<typeof ActionsObject1 &amp; typeof ActionsObject2>;
 */
export type ActionsUnion<A extends ActionCreatorsMapObject> = ReturnType<
  A[keyof A]
>;

The first function uses a type named ActionsWithPayload. The function must be used when you create a new action that carry a payload. It will take two generics type. The former is the string literal of your type: a unique identifier for your action. The latter is the type of the payload. For example, you can set a field of an entity, like the name of a person, by using the following code.

setName: createActionPayload<typeof ACTION_SET_NAME, string>(ACTION_SET_NAME)

The setName is the strongly typed function of the action that you use to dispatch later on. It is linked to the specific string literal ACTION_SET_NAME (unique identifier) and the payload that can be passed to the action is only a string. You invoke the action by calling that function with the payload that you desire:

dispatch(setName("MyNewNameHere"));

The return of the function createActionPayload is an object with the payload and the type — both strongly typed. It allows to have in your reducer a comparison on the type which is unique because it is not a string — but a string literal type. To accomplish this feat, you must define one type per action. That is why, in your reducer file (or the file you want to store all your actions) you must defined a one line per action

export const ACTION_INCREASE_COUNT = "ACTION_INCREASE_COUNT";
export const ACTION_SET_NAME = "ACTION_SET_NAME";

The createActionPayload has also a sibling function createAction that does not take any payload. It works in a similar fashion which is that the type of the action is a unique string literal, but that time without a payload.

To recap what we need to far: first, we need to create a constant that is a string literal that will be used as a unique identifier of the action. Second, we need to create a function that has a payload strongly typed by the action’s type. To tidy up everything, I usually group all common action in a single object.

export const AppActions = {
  increaseCount: createAction<typeof ACTION_INCREASE_COUNT>(
    ACTION_INCREASE_COUNT
  ),
  setName: createActionPayload<typeof ACTION_SET_NAME, string>(ACTION_SET_NAME)
};

It changes slightly how to invoke the action which is clearer. It “namespaces” the action.

dispatch(setName("MyNewNameHere")); // Before
dispatch(AppActions.setName("MyNewNameHere")); // After

Reducer

The reducer is exactly like with Redux, it is a function that takes a state and an action. However, our actions will be constrained to the group of action we allow for the reducer.

export function appReducer(
  state: AppReducerState,
  action: AcceptedActions
): AppReducerState {
// ... 
}

Because we have a strongly typed function per action, we can now define a collection of allowed actions. It helps when you have several reducers in an application to limit the scope of what is expected. If you are tidying the action in an object as proposed, you can do:

export type AcceptedActions = ActionsUnion<typeof AppActions>;

The AcceptedActions type is from the type you add in the ActionsUnion. It means you can add several group of action if you desire with the following syntax.

ActionsUnion<typeof ActionsObject1 &amp; typeof ActionsObject2>;

Here is the complete reducer, like any reducer that you are used to code. However, the switch case only accept cases with the name that are from the AcceptedState which clarify what is possible or not to reduce.

export function appReducer(
  state: AppReducerState,
  action: AcceptedActions
): AppReducerState {
  switch (action.type) {
    case ACTION_INCREASE_COUNT:
      return {
        ...state,
        clickCount: state.clickCount + 1
      };
    case ACTION_SET_NAME:
      return {
        ...state,
        activeEntity: { ...state.activeEntity, ...{ name: action.payload } }
      };
  }
  return state;
}

React Hooks useReducer

The last piece of the puzzle is how to consume the data from the reducer’s state and how to mutate the value. The useReducer function is a React Hooks that take the reducer function and the initial state as parameter. It returns the state and the dispatcher funciton.

const [state, dispatch] = useReducer(appReducer, appState);

To read the value of the state, it is a matter to use the variable state. Because the state is strongly typed, you will get your IDE support as well as static validation from TypeScript. Same for the invocation.

      <button
        onClick={() => {
          dispatch(AppActions.increaseCount());
        }}
      >{`Increase State Count ${state.clickCount}`}</button>

The dispatch requires a function. We reuse the tidy object to select the action. The object is strongly typed, hence when typing you get a nice autocompletion of what action can be dispatched. You avoid any potential mistake of using a variable that is not supported by the reducer. In that example, there is no payload, but the following example shows a strongly typed action with a strongly typed argument.

      <button
        onClick={() => {
          dispatch(AppActions.setName("test"));
        }}
      >

Conclusion

With about 50 lines of TypeScript, you get few utility functions and types to handle React Hooks’s reducer with TypeScript in a pleasant way. As a developer, a new action is a matter of adding a line to uniquely identifier the action, and to define a one-line function to associate the action to a payload type. The reducer is strongly typed, the dispatch of action is also strongly typed. The solution scales well with multiple reducers, it works well with clarifying what can be dispatched and ensure consistency within the application by having a design that group actions of a similar domain together.