From 182e84984223e19a94ebc7076758f9e9f56d1e6e Mon Sep 17 00:00:00 2001 From: Bhargav Ponnapalli Date: Tue, 9 Aug 2022 15:01:15 +0530 Subject: [PATCH] useArraystate and useSetState (#1223) * feat(usearraystate): added a new hook to manage the contents of an array * feat(usesetstate): manage state of a set in React hook * chore(build): remove unnecessary import * Update packages/rooks/src/hooks/useArrayState.ts Co-authored-by: Qiushi Pan <17402261+qqpann@users.noreply.github.com> * chore(pr): address review * feat(changeset): add Co-authored-by: Qiushi Pan <17402261+qqpann@users.noreply.github.com> --- .changeset/dirty-clocks-happen.md | 5 + .husky/_/husky.sh | 36 ++++++ data/docs/useArrayState.md | 19 +++ data/docs/useSetState.md | 19 +++ data/hooks-list.json | 10 ++ .../rooks/src/__tests__/useArrayState.spec.ts | 92 +++++++++++++++ .../rooks/src/__tests__/useSetState.spec.ts | 47 ++++++++ packages/rooks/src/hooks/useArrayState.ts | 111 ++++++++++++++++++ packages/rooks/src/hooks/useSetState.ts | 63 ++++++++++ packages/rooks/src/index.ts | 2 + 10 files changed, 404 insertions(+) create mode 100644 .changeset/dirty-clocks-happen.md create mode 100644 .husky/_/husky.sh create mode 100644 data/docs/useArrayState.md create mode 100644 data/docs/useSetState.md create mode 100644 packages/rooks/src/__tests__/useArrayState.spec.ts create mode 100644 packages/rooks/src/__tests__/useSetState.spec.ts create mode 100644 packages/rooks/src/hooks/useArrayState.ts create mode 100644 packages/rooks/src/hooks/useSetState.ts diff --git a/.changeset/dirty-clocks-happen.md b/.changeset/dirty-clocks-happen.md new file mode 100644 index 0000000000..a9088a5c37 --- /dev/null +++ b/.changeset/dirty-clocks-happen.md @@ -0,0 +1,5 @@ +--- +"rooks": minor +--- + +add useArrayState and useSetState hooks to manage arrays and sets diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100644 index 0000000000..cec959a6b9 --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + if [ $exitCode = 127 ]; then + echo "husky - command not found in PATH=$PATH" + fi + + exit $exitCode +fi diff --git a/data/docs/useArrayState.md b/data/docs/useArrayState.md new file mode 100644 index 0000000000..23eb47309f --- /dev/null +++ b/data/docs/useArrayState.md @@ -0,0 +1,19 @@ +--- +id: useArrayState +title: useArrayState +sidebar_label: useArrayState +--- + +## About + +Array state manager hook for React. It exposes push, pop, unshift, shift, concat, fill and reverse methods to be able to easily modify the state of an array. + +## Examples + +```jsx +import { useArrayState } from "rooks"; +export default function App() { + const [array, controls] = useArrayState([1, 2, 3]); + return null; +} +``` diff --git a/data/docs/useSetState.md b/data/docs/useSetState.md new file mode 100644 index 0000000000..fe9c13599a --- /dev/null +++ b/data/docs/useSetState.md @@ -0,0 +1,19 @@ +--- +id: useSetState +title: useSetState +sidebar_label: useSetState +--- + +## About + +Manage the state of a Set in React. Exposes add, delete and clear methods along with the current set to easily manipulate values in a set. + +## Examples + +```jsx +import { useSetState } from "rooks"; +export default function App() { + useSetState(); + return null; +} +``` diff --git a/data/hooks-list.json b/data/hooks-list.json index c2f77f8447..fb416b2ce5 100644 --- a/data/hooks-list.json +++ b/data/hooks-list.json @@ -1,5 +1,10 @@ { "hooks": [ + { + "name": "useArrayState", + "description": "Array state manager hook for React", + "category": "state" + }, { "name": "useAsyncEffect", "description": "A version of useEffect that accepts an async function", @@ -275,6 +280,11 @@ "description": "useState but syncs with sessionstorage", "category": "state" }, + { + "name": "useSetState", + "description": "Manage the state of a Set in React. ", + "category": "state" + }, { "name": "useStackState", "description": "A React hook that manages state in the form of a stack", diff --git a/packages/rooks/src/__tests__/useArrayState.spec.ts b/packages/rooks/src/__tests__/useArrayState.spec.ts new file mode 100644 index 0000000000..57b9abeab2 --- /dev/null +++ b/packages/rooks/src/__tests__/useArrayState.spec.ts @@ -0,0 +1,92 @@ +import { act } from "@testing-library/react"; +import { renderHook } from "@testing-library/react-hooks"; +import { useArrayState } from "../hooks/useArrayState"; + +describe("useArrayState", () => { + it("should be defined", () => { + expect.hasAssertions(); + expect(useArrayState).toBeDefined(); + }); + + it("should initialize correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + expect(result.current[0]).toEqual([1, 2, 3]); + }); + + it("should push correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].push(4); + }); + + expect(result.current[0]).toEqual([1, 2, 3, 4]); + }); + + it("should pop correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].pop(); + }); + + expect(result.current[0]).toEqual([1, 2]); + }); + + it("should unshift correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].unshift(0); + }); + expect(result.current[0]).toEqual([0, 1, 2, 3]); + }); + + it("should shift correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].shift(); + }); + expect(result.current[0]).toEqual([2, 3]); + }); + + it("should reverse correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].reverse(); + }); + + expect(result.current[0]).toEqual([3, 2, 1]); + }); + + it("should concat correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].concat([4, 5, 6]); + }); + + expect(result.current[0]).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should fill correctly", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useArrayState([1, 2, 3])); + + act(() => { + result.current[1].fill(0); + }); + + expect(result.current[0]).toEqual([0, 0, 0]); + }); +}); diff --git a/packages/rooks/src/__tests__/useSetState.spec.ts b/packages/rooks/src/__tests__/useSetState.spec.ts new file mode 100644 index 0000000000..c4166df3e9 --- /dev/null +++ b/packages/rooks/src/__tests__/useSetState.spec.ts @@ -0,0 +1,47 @@ +import { renderHook, act } from "@testing-library/react"; +import { useSetState } from "../hooks/useSetState"; + +describe("useSetState", () => { + it("should be defined", () => { + expect.hasAssertions(); + expect(useSetState).toBeDefined(); + }); + + it("should initialize the state with the initialSetValue", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useSetState(new Set([1, 2, 3]))); + + expect(result.current[0]).toEqual(new Set([1, 2, 3])); + }); + + it("should add values to the set", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useSetState(new Set([1, 2, 3]))); + + act(() => { + result.current[1].add(4); + }); + expect(result.current[0]).toEqual(new Set([1, 2, 3, 4])); + }); + + it("should delete values from the set", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useSetState(new Set([1, 2, 3]))); + + act(() => { + result.current[1].delete(2); + }); + + expect(result.current[0]).toEqual(new Set([1, 3])); + }); + + it("should clear the set", () => { + expect.hasAssertions(); + const { result } = renderHook(() => useSetState(new Set([1, 2, 3]))); + + act(() => { + result.current[1].clear(); + }); + expect(result.current[0]).toEqual(new Set()); + }); +}); diff --git a/packages/rooks/src/hooks/useArrayState.ts b/packages/rooks/src/hooks/useArrayState.ts new file mode 100644 index 0000000000..fe3e57dcb8 --- /dev/null +++ b/packages/rooks/src/hooks/useArrayState.ts @@ -0,0 +1,111 @@ +import { useCallback, useMemo, useState } from "react"; + +type Push = (...args: Parameters["push"]>) => void; +type Pop = () => void; +type Unshift = (...args: Parameters["unshift"]>) => void; +type Shift = () => void; +type Reverse = () => void; +type Concat = (value: T[]) => void; +type Fill = (value: T, start?: number, end?: number) => void; +type Clear = () => void; + +export type UseArrayStateControls = { + push: Push; + pop: Pop; + clear: Clear; + unshift: Unshift; + shift: Shift; + reverse: Reverse; + concat: Concat; + fill: Fill; +}; + +export type UseArrayStateReturnValue = [T[], UseArrayStateControls]; + +/** + * useArrayState + * @description Array state manager hook for React + * @param {Array} initialState Initial state of the array + * @returns {UseArrayStateReturnValue} Array state manager hook for React + * @see {@link https://react-hooks.org/docs/useArrayState} + * + * @example + * + * const [array, controls] = useArrayState([1, 2, 3]); + * + * controls.push(4); // [1, 2, 3, 4] + * controls.pop(); // [1, 2, 3] + * controls.unshift(0); // [0, 1, 2, 3] + * controls.shift(); // [1, 2, 3] + * controls.reverse(); // [3, 2, 1] + * controls.concat([4, 5, 6]); // [3, 2, 1, 4, 5, 6] + * controls.fill(0); // [0, 0, 0, 0, 0, 0] + */ +function useArrayState(initialArray: T[] = []): UseArrayStateReturnValue { + const [array, setArray] = useState(initialArray); + + const push = useCallback>( + (value) => { + setArray([...array, value]); + }, + [array] + ); + + const pop = useCallback(() => { + setArray(array.slice(0, array.length - 1)); + }, [array]); + + const clear = useCallback(() => { + setArray([]); + }, []); + + const unshift = useCallback>( + (value) => { + setArray([value, ...array]); + }, + [array] + ); + + const shift = useCallback(() => { + setArray(array.slice(1)); + }, [array]); + + const reverse = useCallback(() => { + setArray([...array].reverse()); + }, [array]); + + const concat = useCallback>( + (value: T[]) => { + setArray([...array, ...value]); + }, + [array] + ); + + const fill = useCallback>( + (value: T, start?: number, end?: number) => { + setArray([...array].fill(value, start, end)); + }, + [array] + ); + + const controls = useMemo>(() => { + return { + push, + pop, + clear, + unshift, + shift, + reverse, + concat, + fill, + }; + }, [push, pop, clear, unshift, shift, reverse, concat, fill]); + + const returnValue = useMemo>(() => { + return [array, controls]; + }, [array, controls]); + + return returnValue; +} + +export { useArrayState }; diff --git a/packages/rooks/src/hooks/useSetState.ts b/packages/rooks/src/hooks/useSetState.ts new file mode 100644 index 0000000000..b4fecb5c08 --- /dev/null +++ b/packages/rooks/src/hooks/useSetState.ts @@ -0,0 +1,63 @@ +import { useCallback, useMemo, useState } from "react"; + +type Add = (...args: Parameters["add"]>) => void; +type Delete = (...args: Parameters["delete"]>) => void; + +export type UseSetStateControls = { + add: Add; + delete: Delete; + clear: () => void; +}; + +export type UseSetStateReturnValue = [Set, UseSetStateControls]; + +/** + * useSetState + * @description Manage the state of a Set in React. + * @param {Set} initialSetValue The initial value of the set to manage. + * @returns {UseSetStateReturnValue} The state of the Set and the controls. + * @see {@link https://react-hooks.org/docs/useSetState} + * @example + * import { useSetState } from "@/hooks/useSetState"; + * const [set, setControls] = useSetState(new Set()); + * setControls.add(1); // {1} + * setControls.add(2); // {1, 2} + * setControls.delete(1); // {2} + * setControls.clear(); // {} + * + */ +function useSetState(initialSetValue: Set): UseSetStateReturnValue { + const [setValue, setSetValue] = useState>(new Set(initialSetValue)); + + const add = useCallback>( + (...args): void => { + setSetValue(new Set(setValue.add(...args))); + }, + [setValue, setSetValue] + ); + + const deleteValue = useCallback>( + (...args): void => { + const newSetValue = new Set(setValue); + newSetValue.delete(...args); + setSetValue(newSetValue); + }, + [setValue, setSetValue] + ); + + const clear = useCallback((): void => { + setSetValue(new Set()); + }, [setSetValue]); + + const controls = useMemo>(() => { + return { add, delete: deleteValue, clear }; + }, [add, deleteValue, clear]); + + const returnValue = useMemo>(() => { + return [setValue, controls]; + }, [setValue, controls]); + + return returnValue; +} + +export { useSetState }; diff --git a/packages/rooks/src/index.ts b/packages/rooks/src/index.ts index d66e612cc4..196bba55d3 100644 --- a/packages/rooks/src/index.ts +++ b/packages/rooks/src/index.ts @@ -1,3 +1,4 @@ +export { useArrayState } from "./hooks/useArrayState"; export { useAsyncEffect } from "./hooks/useAsyncEffect"; export { useBoundingclientrect } from "./hooks/useBoundingclientrect"; export { useBoundingclientrectRef } from "./hooks/useBoundingclientrectRef"; @@ -53,6 +54,7 @@ export { useRefElement } from "./hooks/useRefElement"; export { useSelect } from "./hooks/useSelect"; export { useSelectableList } from "./hooks/useSelectableList"; export { useSessionstorageState } from "./hooks/useSessionstorageState"; +export { useSetState } from "./hooks/useSetState"; export { useStackState } from "./hooks/useStackState"; export { useThrottle } from "./hooks/useThrottle"; export { useTimeoutWhen } from "./hooks/useTimeoutWhen";