Skip to content

Commit

Permalink
feat: useObjectState hook
Browse files Browse the repository at this point in the history
  • Loading branch information
congweibai committed Aug 19, 2024
1 parent 865196f commit acc2d52
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./useQueue/useQueue";
export * from "./useTimeout";
export * from "./useWindowSize";
export * from "./useVisibilityChange";
export * from "./useObjectState";
1 change: 1 addition & 0 deletions src/hooks/useObjectState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useObjectState";
86 changes: 86 additions & 0 deletions src/hooks/useObjectState/useObjectState.test.tsx
Original file line number Diff line number Diff line change
@@ -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({});
});
});
36 changes: 36 additions & 0 deletions src/hooks/useObjectState/useObjectState.tsx
Original file line number Diff line number Diff line change
@@ -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<Object>(
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];
}

0 comments on commit acc2d52

Please sign in to comment.