Skip to content

Commit

Permalink
Add use-reducer-with-callback hook
Browse files Browse the repository at this point in the history
  • Loading branch information
luisdralves committed Jul 15, 2022
1 parent a0ca53e commit 416b7ae
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 0 deletions.
87 changes: 87 additions & 0 deletions packages/hooks/src/use-reducer-with-callback/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Module dependencies.
*/

import { act, renderHook } from '@testing-library/react-hooks';
import { useReducerWithCallback } from './';

/**
* Mock reducer `State` type.
*/

type State = {
bar: number;
foo: number;
};

/**
* Mock reducer `Action` type.
*/

type Action = 'incFoo' | 'incBar' | 'decFoo' | 'doubleBar';

/**
* Mock reducer.
*/

const reducer = (state: State, action: Action) => {
switch (action) {
case 'incFoo':
return { ...state, foo: state.foo + 1 };

case 'incBar':
return { ...state, bar: state.bar + 1 };

case 'decFoo':
return { ...state, foo: state.foo - 1 };

case 'doubleBar':
return { ...state, bar: state.bar * 2 };

default:
return state;
}
};

/**
* Test `useReducerWithCallback` hook.
*/

describe(`'useReducerWithCallback' hook`, () => {
it('should run the callback after the dispatch', () => {
const { result } = renderHook(() => {
const [state, dispatchWithCallback] = useReducerWithCallback<
State,
Action
>(reducer, { bar: 0, foo: 0 });

return { dispatchWithCallback, state };
});

act(() => {
result.current.dispatchWithCallback('incBar', () => {
// Unlike the next assertion, this one only runs after the dispatch
expect(result.current.state).toEqual({ bar: 1, foo: 0 });
});

// This assertion runs in the same render as the dispatch, and as such the state has not been updated yet
expect(result.current.state).toEqual({ bar: 0, foo: 0 });
});

act(() => {
result.current.dispatchWithCallback('doubleBar', () => {
expect(result.current.state).toEqual({ bar: 2, foo: 0 });
});

expect(result.current.state).toEqual({ bar: 1, foo: 0 });
});

act(() => {
result.current.dispatchWithCallback('decFoo', () => {
expect(result.current.state).toEqual({ bar: 2, foo: -1 });
});

expect(result.current.state).toEqual({ bar: 2, foo: 0 });
});
});
});
54 changes: 54 additions & 0 deletions packages/hooks/src/use-reducer-with-callback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Module dependencies.
*/

import { Reducer, useCallback, useEffect, useReducer, useRef } from 'react';

/**
* Export `DispatchWithCallback` type.
*/

export type DispatchWithCallback<State, Action> = (
action: Action,
callback?: (state: State) => void
) => void;

/**
* `Return` type.
*/

type Return<State, Action> = [State, DispatchWithCallback<State, Action>];

/**
* Export `useReducerWithCallback` hook.
*/

export function useReducerWithCallback<State, Action>(
reducer: (state: State, action: Action) => State,
initialState: State
): Return<State, Action> {
const [state, dispatch] = useReducer<Reducer<State, Action>>(
reducer,
initialState
);

const callbackRef = useRef<((state: State) => void) | null>(null);
const dispatchWithCallback = useCallback(
(action: Action, callback?: (state: State) => void) => {
callbackRef.current = callback ?? null;

return dispatch(action);
},
[]
);

useEffect(() => {
if (callbackRef.current) {
callbackRef.current(state);

callbackRef.current = null;
}
}, [state]);

return [state, dispatchWithCallback];
}

0 comments on commit 416b7ae

Please sign in to comment.