From 7554b9a61eb9b4744b9feb113775ff538b16beaf Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Sat, 18 Jan 2020 08:16:38 +0530 Subject: [PATCH] feat: add useMethods state hook --- docs/useMethods.md | 48 +++++++++++++++++++++ src/index.ts | 1 + src/useMethods.ts | 33 +++++++++++---- stories/useMethods.story.tsx | 38 +++++++++++++++++ tests/useMethods.test.ts | 81 ++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 docs/useMethods.md create mode 100644 stories/useMethods.story.tsx create mode 100644 tests/useMethods.test.ts diff --git a/docs/useMethods.md b/docs/useMethods.md new file mode 100644 index 0000000000..718f80e617 --- /dev/null +++ b/docs/useMethods.md @@ -0,0 +1,48 @@ +# `useMethods` + +React hook that simplifies the `useReducer` implementation. + +## Usage + +```jsx +import { useMethods } from 'react-use'; + +const initialState = { + count: 0, +}; + +function createMethods(state) { + return { + reset() { + return initialState; + }, + increment() { + return { ...state, count: state.count + 1 }; + }, + decrement() { + return { ...state, count: state.count - 1 }; + }, + }; +} + +const Demo = () => { + const [state, methods] = useMethods(createMethods, initialState); + + return ( + <> +

Count: {state.count}

+ + + + ); +}; +``` + +## Reference + +```js +const [state, methods] = useMethods(createMethods, initialState); +``` + +- `createMethods` — function that takes current state and return an object containing methods that return updated state. +- `initialState` — initial value of the state. diff --git a/src/index.ts b/src/index.ts index 64828fb1c6..70e290a965 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ export { default as useMap } from './useMap'; export { default as useMedia } from './useMedia'; export { default as useMediaDevices } from './useMediaDevices'; export { useMediatedState } from './useMediatedState'; +export { default as useMethods } from './useMethods'; export { default as useMotion } from './useMotion'; export { default as useMount } from './useMount'; export { default as useMountedState } from './useMountedState'; diff --git a/src/useMethods.ts b/src/useMethods.ts index 9b29d99147..8483f7ba7a 100644 --- a/src/useMethods.ts +++ b/src/useMethods.ts @@ -1,23 +1,38 @@ -/* eslint-disable */ -import { useMemo, useReducer } from 'react'; +import { useMemo, useReducer, Reducer } from 'react'; -const useMethods = (createMethods, initialState) => { - const reducer = useMemo( - () => (reducerState, action) => { +type Action = { + type: string; + payload?: any; +}; + +type CreateMethods = ( + state: T +) => { + [P in keyof M]: (payload?: any) => T; +}; + +type WrappedMethods = { + [P in keyof M]: (...payload: any) => void; +}; + +const useMethods = (createMethods: CreateMethods, initialState: T): [T, WrappedMethods] => { + const reducer = useMemo>( + () => (reducerState: T, action: Action) => { return createMethods(reducerState)[action.type](...action.payload); }, [createMethods] ); - const [state, dispatch] = useReducer(reducer, initialState); + const [state, dispatch] = useReducer>(reducer, initialState); - const wrappedMethods = useMemo(() => { + const wrappedMethods: WrappedMethods = useMemo(() => { const actionTypes = Object.keys(createMethods(initialState)); + return actionTypes.reduce((acc, type) => { acc[type] = (...payload) => dispatch({ type, payload }); return acc; - }, {}); - }, []); + }, {} as WrappedMethods); + }, [createMethods]); return [state, wrappedMethods]; }; diff --git a/stories/useMethods.story.tsx b/stories/useMethods.story.tsx new file mode 100644 index 0000000000..8104297cd6 --- /dev/null +++ b/stories/useMethods.story.tsx @@ -0,0 +1,38 @@ +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import { useMethods } from '../src'; +import ShowDocs from './util/ShowDocs'; + +const initialState = { + count: 0, +}; + +function createMethods(state) { + return { + reset() { + return initialState; + }, + increment() { + return { ...state, count: state.count + 1 }; + }, + decrement() { + return { ...state, count: state.count - 1 }; + }, + }; +} + +const Demo = () => { + const [state, methods] = useMethods(createMethods, initialState); + + return ( + <> +

Count: {state.count}

+ + + + ); +}; + +storiesOf('State|useMethods', module) + .add('Docs', () => ) + .add('Demo', () => ); diff --git a/tests/useMethods.test.ts b/tests/useMethods.test.ts new file mode 100644 index 0000000000..c34394e3cc --- /dev/null +++ b/tests/useMethods.test.ts @@ -0,0 +1,81 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useMethods } from '../src'; + +it('should have initialState value as the returned state value', () => { + const initialState = { + count: 10, + }; + + const createMethods = state => ({ + doStuff: () => state, + }); + + const { result } = renderHook(() => useMethods(createMethods, initialState)); + + expect(result.current[0]).toEqual(initialState); +}); + +it('should return wrappedMethods object containing all the methods defined in createMethods', () => { + const initialState = { + count: 10, + }; + + const createMethods = state => ({ + reset() { + return initialState; + }, + increment() { + return { ...state, count: state.count + 1 }; + }, + decrement() { + return { ...state, count: state.count - 1 }; + }, + }); + + const { result } = renderHook(() => useMethods(createMethods, initialState)); + + for (const key of Object.keys(createMethods(initialState))) { + expect(result.current[1][key]).toBeDefined(); + } +}); + +it('should properly update the state based on the createMethods', () => { + const count = 10; + const initialState = { + count, + }; + + const createMethods = state => ({ + reset() { + return initialState; + }, + increment() { + return { ...state, count: state.count + 1 }; + }, + decrement() { + return { ...state, count: state.count - 1 }; + }, + }); + + const { result } = renderHook(() => useMethods(createMethods, initialState)); + + act(() => { + result.current[1].increment(); + }); + expect(result.current[0].count).toBe(count + 1); + + act(() => { + result.current[1].decrement(); + }); + expect(result.current[0].count).toBe(count); + + act(() => { + result.current[1].decrement(); + }); + expect(result.current[0].count).toBe(count - 1); + + act(() => { + result.current[1].reset(); + }); + expect(result.current[0].count).toBe(count); +});