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);
+});