Skip to content

Commit

Permalink
feat: add useMethods state hook
Browse files Browse the repository at this point in the history
  • Loading branch information
ayush987goyal committed Feb 15, 2020
1 parent 57d2546 commit 7554b9a
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 9 deletions.
48 changes: 48 additions & 0 deletions docs/useMethods.md
Original file line number Diff line number Diff line change
@@ -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 (
<>
<p>Count: {state.count}</p>
<button onClick={methods.decrement}>-</button>
<button onClick={methods.increment}>+</button>
</>
);
};
```

## Reference

```js
const [state, methods] = useMethods(createMethods, initialState);
```

- `createMethods` &mdash; function that takes current state and return an object containing methods that return updated state.
- `initialState` &mdash; initial value of the state.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
33 changes: 24 additions & 9 deletions src/useMethods.ts
Original file line number Diff line number Diff line change
@@ -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<M, T> = (
state: T
) => {
[P in keyof M]: (payload?: any) => T;
};

type WrappedMethods<M> = {
[P in keyof M]: (...payload: any) => void;
};

const useMethods = <M, T>(createMethods: CreateMethods<M, T>, initialState: T): [T, WrappedMethods<M>] => {
const reducer = useMemo<Reducer<T, Action>>(
() => (reducerState: T, action: Action) => {
return createMethods(reducerState)[action.type](...action.payload);
},
[createMethods]
);

const [state, dispatch] = useReducer(reducer, initialState);
const [state, dispatch] = useReducer<Reducer<T, Action>>(reducer, initialState);

const wrappedMethods = useMemo(() => {
const wrappedMethods: WrappedMethods<M> = useMemo(() => {
const actionTypes = Object.keys(createMethods(initialState));

return actionTypes.reduce((acc, type) => {
acc[type] = (...payload) => dispatch({ type, payload });
return acc;
}, {});
}, []);
}, {} as WrappedMethods<M>);
}, [createMethods]);

return [state, wrappedMethods];
};
Expand Down
38 changes: 38 additions & 0 deletions stories/useMethods.story.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<p>Count: {state.count}</p>
<button onClick={methods.decrement}>-</button>
<button onClick={methods.increment}>+</button>
</>
);
};

storiesOf('State|useMethods', module)
.add('Docs', () => <ShowDocs md={require('../docs/useMethods.md')} />)
.add('Demo', () => <Demo />);
81 changes: 81 additions & 0 deletions tests/useMethods.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 7554b9a

Please sign in to comment.