Skip to content

Commit 037273f

Browse files
authored
Merge pull request #366 from Zaccal/useObject
useObject 🧊 [feat]: Create hook useObject
2 parents 5f15ed2 + 55c8657 commit 037273f

5 files changed

Lines changed: 252 additions & 0 deletions

File tree

packages/core/src/bundle/hooks/state.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './useList/useList';
1212
// storage
1313
export * from './useLocalStorage/useLocalStorage';
1414
export * from './useMap/useMap';
15+
export * from './useObject/useObject';
1516
export * from './useOffsetPagination/useOffsetPagination';
1617
export * from './useQuery/useQuery';
1718
export * from './useQueue/useQueue';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useState } from 'react';
2+
/**
3+
* @name useObject
4+
* @description - Hook that provides state and helper methods to manage an object
5+
* @category State
6+
*
7+
* @template T The type of the object
8+
* @param {T} initialValue The initial object value
9+
* @returns {UseObjectReturn<T>} An object containing the current state and functions to interact with the object
10+
*
11+
* @example
12+
* const { state, set, get, reset, update, merge, remove } = useObject({ a: 1, b: 2 });
13+
*/
14+
export function useObject(initialValue) {
15+
const [state, setState] = useState(initialValue);
16+
const set = (key, value) => {
17+
setState((prev) => ({
18+
...prev,
19+
[key]: value
20+
}));
21+
};
22+
const get = (key) => {
23+
return state[key];
24+
};
25+
const reset = () => {
26+
setState(initialValue);
27+
};
28+
const update = (updater) => {
29+
setState((prev) => updater(prev));
30+
};
31+
const merge = (newState) => {
32+
setState((prev) => ({
33+
...prev,
34+
...newState
35+
}));
36+
};
37+
const remove = (key) => {
38+
setState((prev) => {
39+
const { [key]: _, ...rest } = prev;
40+
return rest;
41+
});
42+
};
43+
return {
44+
state,
45+
set,
46+
get,
47+
reset,
48+
update,
49+
merge,
50+
remove
51+
};
52+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useObject } from './useObject';
2+
3+
const Demo = () => {
4+
const { state, get, set, update, merge, remove, reset } = useObject({
5+
name: 'Vladislav',
6+
age: '32',
7+
email: 'testmail@mail.com'
8+
});
9+
10+
return (
11+
<div className='flex items-start gap-4'>
12+
<div className=''>
13+
<input
14+
type='text'
15+
value={state.name}
16+
onChange={(event) => set('name', event.target.value)}
17+
placeholder='Name'
18+
/>
19+
<input
20+
type='number'
21+
value={state.age}
22+
onChange={(event) => set('age', event.target.value)}
23+
placeholder='Age'
24+
/>
25+
<input
26+
type='email'
27+
value={state.email}
28+
onChange={(event) => set('email', event.target.value)}
29+
placeholder='Email'
30+
/>
31+
<div className=''>
32+
<button onClick={reset}>Reset</button>
33+
<button onClick={() => alert(get('email'))}>Get Email</button>
34+
<button onClick={() => update((prev) => ({ ...prev, age: '10' }))}>Update Age</button>
35+
<button onClick={() => merge({ name: 'Updated name', age: '10' })}>
36+
Merge Name and Age
37+
</button>
38+
<button onClick={() => remove('email')}>Remove Email</button>
39+
</div>
40+
</div>
41+
<pre>
42+
<code>{JSON.stringify(state, null, 2)}</code>
43+
</pre>
44+
</div>
45+
);
46+
};
47+
48+
export default Demo;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
3+
import { useObject } from './useObject';
4+
5+
const INITIAL_OBJECT = { a: 1, b: 2, c: 3 };
6+
7+
describe('useObject', () => {
8+
it('Should use object', () => {
9+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
10+
const obj = result.current;
11+
12+
expect(obj.state).toEqual(INITIAL_OBJECT);
13+
expect(obj.set).toBeTypeOf('function');
14+
expect(obj.get).toBeTypeOf('function');
15+
expect(obj.reset).toBeTypeOf('function');
16+
expect(obj.update).toBeTypeOf('function');
17+
expect(obj.merge).toBeTypeOf('function');
18+
expect(obj.remove).toBeTypeOf('function');
19+
});
20+
21+
it('Should set property', () => {
22+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
23+
const obj = result.current;
24+
25+
act(() => obj.set('a', 42));
26+
27+
expect(result.current.state).toEqual({ a: 42, b: 2, c: 3 });
28+
});
29+
30+
it('Should get property', () => {
31+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
32+
const obj = result.current;
33+
34+
expect(obj.get('b')).toBe(2);
35+
});
36+
37+
it('Should reset object', () => {
38+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
39+
const obj = result.current;
40+
41+
act(() => obj.set('a', 99));
42+
act(() => obj.reset());
43+
44+
expect(result.current.state).toEqual(INITIAL_OBJECT);
45+
});
46+
47+
it('Should update object', () => {
48+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
49+
const obj = result.current;
50+
51+
act(() => obj.update((prev) => ({ ...prev, d: 4 }) as typeof INITIAL_OBJECT & { d: number }));
52+
53+
expect(result.current.state).toEqual({ a: 1, b: 2, c: 3, d: 4 });
54+
});
55+
56+
it('Should merge object', () => {
57+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
58+
const obj = result.current;
59+
60+
act(() => obj.merge({ b: 10 }));
61+
62+
expect(result.current.state).toEqual({ a: 1, b: 10, c: 3 });
63+
});
64+
65+
it('Should remove property', () => {
66+
const { result } = renderHook(() => useObject(INITIAL_OBJECT));
67+
const obj = result.current;
68+
69+
act(() => obj.remove('b'));
70+
71+
expect(result.current.state).toEqual({ a: 1, c: 3 });
72+
});
73+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useState } from 'react';
2+
3+
/** The useObject return type */
4+
export interface UseObjectReturn<T extends object> {
5+
/** The current object state */
6+
state: T;
7+
/** Gets a property from the object */
8+
get: (key: keyof T) => T[keyof T];
9+
/** Merges a partial object into the state */
10+
merge: (newState: Partial<T>) => void;
11+
/** Removes a property from the object */
12+
remove: (key: keyof T) => void;
13+
/** Resets the object to its initial value */
14+
reset: () => void;
15+
/** Sets a property on the object */
16+
set: (key: keyof T, value: T[keyof T]) => void;
17+
/** Updates the object using an updater function */
18+
update: (updater: (prev: T) => T) => void;
19+
}
20+
21+
/**
22+
* @name useObject
23+
* @description - Hook that provides state and helper methods to manage an object
24+
* @category State
25+
*
26+
* @template T The type of the object
27+
* @param {T} initialValue The initial object value
28+
* @returns {UseObjectReturn<T>} An object containing the current state and functions to interact with the object
29+
*
30+
* @example
31+
* const { state, set, get, reset, update, merge, remove } = useObject({ a: 1, b: 2 });
32+
*/
33+
export function useObject<T extends object>(initialValue: T): UseObjectReturn<T> {
34+
const [state, setState] = useState<T>(initialValue);
35+
36+
const set = (key: keyof T, value: T[keyof T]) => {
37+
setState((prev) => ({
38+
...prev,
39+
[key]: value
40+
}));
41+
};
42+
43+
const get = (key: keyof T): T[keyof T] => {
44+
return state[key];
45+
};
46+
47+
const reset = () => {
48+
setState(initialValue);
49+
};
50+
51+
const update = (updater: (prev: T) => T) => {
52+
setState((prev) => updater(prev));
53+
};
54+
55+
const merge = (newState: Partial<T>) => {
56+
setState((prev) => ({
57+
...prev,
58+
...newState
59+
}));
60+
};
61+
62+
const remove = (key: keyof T) => {
63+
setState((prev) => {
64+
const { [key]: _, ...rest } = prev;
65+
return rest as T;
66+
});
67+
};
68+
69+
return {
70+
state,
71+
set,
72+
get,
73+
reset,
74+
update,
75+
merge,
76+
remove
77+
};
78+
}

0 commit comments

Comments
 (0)