From b694580a148d9a4eac66a41d1354d5fbf7853bf7 Mon Sep 17 00:00:00 2001 From: Wyatt Bai <53634020+congweibai@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:30:21 +0930 Subject: [PATCH] feat: useObjectState hook --- src/hooks/index.ts | 1 + src/hooks/useObjectState/index.ts | 1 + .../useObjectState/useObjectState.test.tsx | 86 +++++++++++++++++++ src/hooks/useObjectState/useObjectState.tsx | 36 ++++++++ 4 files changed, 124 insertions(+) create mode 100644 src/hooks/useObjectState/index.ts create mode 100644 src/hooks/useObjectState/useObjectState.test.tsx create mode 100644 src/hooks/useObjectState/useObjectState.tsx diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 22c69bf..639583d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -10,3 +10,4 @@ export * from "./useQueue/useQueue"; export * from "./useTimeout"; export * from "./useWindowSize"; export * from "./useVisibilityChange"; +export * from "./useObjectState"; diff --git a/src/hooks/useObjectState/index.ts b/src/hooks/useObjectState/index.ts new file mode 100644 index 0000000..d7a9446 --- /dev/null +++ b/src/hooks/useObjectState/index.ts @@ -0,0 +1 @@ +export * from "./useObjectState"; diff --git a/src/hooks/useObjectState/useObjectState.test.tsx b/src/hooks/useObjectState/useObjectState.test.tsx new file mode 100644 index 0000000..164ec1a --- /dev/null +++ b/src/hooks/useObjectState/useObjectState.test.tsx @@ -0,0 +1,86 @@ +import { renderHook, act } from "@testing-library/react-hooks"; +import useObjectState from "./useObjectState"; +import { describe, it, expect } from "vitest"; + +describe("useObjectState", () => { + it("should initialize with the provided initial value", () => { + // mock + const initialValue = { key1: "value1", key2: "value2" }; + + // act + const { result } = renderHook(() => useObjectState(initialValue)); + + // assert + const [state] = result.current; + expect(state).toEqual(initialValue); + }); + + it("should update state by merging new values", () => { + // mock + const initialValue = { key1: "value1", key2: "value2" }; + const { result } = renderHook(() => useObjectState(initialValue)); + + // act + const [, setObjectState] = result.current; + + act(() => { + setObjectState({ key2: "newValue2", key3: "value3" }); + }); + + // assert + const [state] = result.current; + expect(state).toEqual({ + key1: "value1", + key2: "newValue2", + key3: "value3", + }); + }); + + it("should update state using a function", () => { + // mock + const initialValue = { key1: "value1", count: 1 }; + const { result } = renderHook(() => useObjectState(initialValue)); + + // act + const [, setObjectState] = result.current; + + act(() => { + setObjectState((prevState) => ({ + count: prevState.count + 1, + })); + }); + + // assert + const [state] = result.current; + expect(state).toEqual({ + key1: "value1", + count: 2, + }); + }); + + it("should not update state when given a non-object value", () => { + // mock + const initialValue = { key1: "value1" }; + const { result } = renderHook(() => useObjectState(initialValue)); + + // act + const [, setObjectState] = result.current; + + act(() => { + setObjectState(null); + }); + + // assert + const [state] = result.current; + expect(state).toEqual(initialValue); + }); + + it("should handle updates when initial value is not a plain object", () => { + // act + const { result } = renderHook(() => useObjectState("invalid" as any)); + + // assert + const [state] = result.current; + expect(state).toEqual({}); + }); +}); diff --git a/src/hooks/useObjectState/useObjectState.tsx b/src/hooks/useObjectState/useObjectState.tsx new file mode 100644 index 0000000..eaf3a05 --- /dev/null +++ b/src/hooks/useObjectState/useObjectState.tsx @@ -0,0 +1,36 @@ +import { useCallback, useState } from "react"; + +const isPlainObject = (value: unknown) => { + return Object.prototype.toString.call(value) === "[object Object]"; +}; + +export default function useObjectState(initialValue: Object) { + const [value, setValue] = useState( + isPlainObject(initialValue) ? initialValue : {} + ); + + const setObjectState = useCallback((value: Object) => { + if (typeof value === "function") { + setValue((current) => { + const newValue = value(current); + + if (isPlainObject(newValue)) { + return { + ...current, + ...newValue, + }; + } + }); + } + if (isPlainObject(value)) { + setValue((current) => { + return { + ...current, + ...value, + }; + }); + } + }, []); + + return [value, setObjectState]; +}