Skip to content

Commit 68e7ac8

Browse files
sukvvonkimyouknow
authored andcommitted
feat(core/hooks): add 'useList' hook (#341)
1 parent ffc61bb commit 68e7ac8

7 files changed

Lines changed: 407 additions & 0 deletions

File tree

.changeset/swift-birds-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-simplikit': patch
3+
---
4+
5+
feat(core/hooks): add 'useList' hook
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useList } from './useList.ts';
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# useList
2+
3+
리액트 훅으로, 배열을 상태로 관리해요. 효율적인 상태 관리를 제공하고 안정적인 액션 함수를 제공해요.
4+
5+
## 인터페이스
6+
7+
```ts
8+
function useList<T>(initialState?: T[]): UseListReturn<T>;
9+
```
10+
11+
### 파라미터
12+
13+
<Interface
14+
name="initialState"
15+
type="T[]"
16+
description="초기 배열 상태예요. 기본값은 빈 배열이에요."
17+
/>
18+
19+
### 반환 값
20+
21+
튜플 `[list, actions]`를 반환해요.
22+
23+
<Interface name="list" type="ReadonlyArray<T>" description="현재 배열 상태예요." />
24+
25+
<Interface name="actions.push" type="(value: T) => void" description="리스트의 끝에 값을 추가해요." />
26+
<Interface name="actions.insertAt" type="(index: number, value: T) => void" description="지정된 인덱스에 값을 삽입해요." />
27+
<Interface name="actions.updateAt" type="(index: number, value: T) => void" description="지정된 인덱스의 값을 업데이트해요." />
28+
<Interface name="actions.removeAt" type="(index: number) => void" description="지정된 인덱스의 값을 제거해요." />
29+
<Interface name="actions.setAll" type="(values: T[]) => void" description="전체 리스트를 새 배열로 교체해요." />
30+
<Interface name="actions.reset" type="() => void" description="리스트를 초기 상태로 되돌려요." />
31+
32+
## 예시
33+
34+
```tsx
35+
import { useList } from 'react-simplikit';
36+
37+
function TodoList() {
38+
const [todos, actions] = useList<string>(['Buy milk', 'Walk the dog']);
39+
40+
return (
41+
<div>
42+
<ul>
43+
{todos.map((todo, index) => (
44+
<li key={index}>
45+
{todo}
46+
<button onClick={() => actions.removeAt(index)}>Delete</button>
47+
</li>
48+
))}
49+
</ul>
50+
<button onClick={() => actions.push('New todo')}>Add</button>
51+
<button onClick={() => actions.reset()}>Reset</button>
52+
</div>
53+
);
54+
}
55+
```
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# useList
2+
3+
A React hook that manages an array as state. Provides efficient state management and stable action functions.
4+
5+
## Interface
6+
7+
```ts
8+
function useList<T>(initialState?: T[]): UseListReturn<T>;
9+
```
10+
11+
### Parameters
12+
13+
<Interface
14+
name="initialState"
15+
type="T[]"
16+
description="Initial array state. Defaults to an empty array."
17+
/>
18+
19+
### Return Value
20+
21+
Returns a tuple `[list, actions]`.
22+
23+
<Interface name="list" type="ReadonlyArray<T>" description="The current array state." />
24+
25+
<Interface name="actions.push" type="(value: T) => void" description="Appends a value to the end of the list." />
26+
<Interface name="actions.insertAt" type="(index: number, value: T) => void" description="Inserts a value at the specified index." />
27+
<Interface name="actions.updateAt" type="(index: number, value: T) => void" description="Updates the value at the specified index." />
28+
<Interface name="actions.removeAt" type="(index: number) => void" description="Removes the value at the specified index." />
29+
<Interface name="actions.setAll" type="(values: T[]) => void" description="Replaces the entire list with a new array." />
30+
<Interface name="actions.reset" type="() => void" description="Resets the list to its initial state." />
31+
32+
## Example
33+
34+
```tsx
35+
import { useList } from 'react-simplikit';
36+
37+
function TodoList() {
38+
const [todos, actions] = useList<string>(['Buy milk', 'Walk the dog']);
39+
40+
return (
41+
<div>
42+
<ul>
43+
{todos.map((todo, index) => (
44+
<li key={index}>
45+
{todo}
46+
<button onClick={() => actions.removeAt(index)}>Delete</button>
47+
</li>
48+
))}
49+
</ul>
50+
<button onClick={() => actions.push('New todo')}>Add</button>
51+
<button onClick={() => actions.reset()}>Reset</button>
52+
</div>
53+
);
54+
}
55+
```
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { act } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { renderHookSSR } from '../../_internal/test-utils/renderHookSSR.tsx';
5+
6+
import { useList } from './useList.ts';
7+
8+
describe('useList', () => {
9+
it('is safe on server side rendering', () => {
10+
const result = renderHookSSR.serverOnly(() => useList(['a', 'b']));
11+
12+
expect(result.current[0]).toEqual(['a', 'b']);
13+
});
14+
15+
it('should initialize with an array', async () => {
16+
const { result } = await renderHookSSR(() => useList(['a', 'b']));
17+
18+
expect(result.current[0]).toEqual(['a', 'b']);
19+
});
20+
21+
it('should initialize with an empty array when no arguments provided', async () => {
22+
const { result } = await renderHookSSR(() => useList());
23+
24+
expect(result.current[0]).toEqual([]);
25+
});
26+
27+
it('should push a value to the end of the list', async () => {
28+
const { result, rerender } = await renderHookSSR(() => useList(['a']));
29+
const [, actions] = result.current;
30+
31+
await act(async () => {
32+
actions.push('b');
33+
rerender();
34+
});
35+
36+
expect(result.current[0]).toEqual(['a', 'b']);
37+
});
38+
39+
it('should insert a value at the specified index', async () => {
40+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'c']));
41+
const [, actions] = result.current;
42+
43+
await act(async () => {
44+
actions.insertAt(1, 'b');
45+
rerender();
46+
});
47+
48+
expect(result.current[0]).toEqual(['a', 'b', 'c']);
49+
});
50+
51+
it('should insert at the beginning when index is 0', async () => {
52+
const { result, rerender } = await renderHookSSR(() => useList(['b', 'c']));
53+
const [, actions] = result.current;
54+
55+
await act(async () => {
56+
actions.insertAt(0, 'a');
57+
rerender();
58+
});
59+
60+
expect(result.current[0]).toEqual(['a', 'b', 'c']);
61+
});
62+
63+
it('should update a value at the specified index', async () => {
64+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b', 'c']));
65+
const [, actions] = result.current;
66+
67+
await act(async () => {
68+
actions.updateAt(1, 'x');
69+
rerender();
70+
});
71+
72+
expect(result.current[0]).toEqual(['a', 'x', 'c']);
73+
});
74+
75+
it('should remove a value at the specified index', async () => {
76+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b', 'c']));
77+
const [, actions] = result.current;
78+
79+
await act(async () => {
80+
actions.removeAt(1);
81+
rerender();
82+
});
83+
84+
expect(result.current[0]).toEqual(['a', 'c']);
85+
});
86+
87+
it('should remove the first item', async () => {
88+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b', 'c']));
89+
const [, actions] = result.current;
90+
91+
await act(async () => {
92+
actions.removeAt(0);
93+
rerender();
94+
});
95+
96+
expect(result.current[0]).toEqual(['b', 'c']);
97+
});
98+
99+
it('should remove the last item', async () => {
100+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b', 'c']));
101+
const [, actions] = result.current;
102+
103+
await act(async () => {
104+
actions.removeAt(2);
105+
rerender();
106+
});
107+
108+
expect(result.current[0]).toEqual(['a', 'b']);
109+
});
110+
111+
it('should replace all values with setAll', async () => {
112+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b']));
113+
const [, actions] = result.current;
114+
115+
await act(async () => {
116+
actions.setAll(['x', 'y', 'z']);
117+
rerender();
118+
});
119+
120+
expect(result.current[0]).toEqual(['x', 'y', 'z']);
121+
});
122+
123+
it('should reset the list to its initial state', async () => {
124+
const { result, rerender } = await renderHookSSR(() => useList(['a', 'b']));
125+
const [, actions] = result.current;
126+
127+
await act(async () => {
128+
actions.push('c');
129+
actions.removeAt(0);
130+
rerender();
131+
});
132+
133+
expect(result.current[0]).not.toEqual(['a', 'b']);
134+
135+
await act(async () => {
136+
actions.reset();
137+
rerender();
138+
});
139+
140+
expect(result.current[0]).toEqual(['a', 'b']);
141+
});
142+
143+
it('should reset to empty array when initialized with empty array', async () => {
144+
const { result, rerender } = await renderHookSSR(() => useList<string>());
145+
const [, actions] = result.current;
146+
147+
await act(async () => {
148+
actions.push('a');
149+
actions.push('b');
150+
rerender();
151+
});
152+
153+
expect(result.current[0]).toEqual(['a', 'b']);
154+
155+
await act(async () => {
156+
actions.reset();
157+
rerender();
158+
});
159+
160+
expect(result.current[0]).toEqual([]);
161+
});
162+
163+
it('should create a new array reference when values change', async () => {
164+
const { result, rerender } = await renderHookSSR(() => useList(['a']));
165+
const [originalRef] = result.current;
166+
167+
await act(async () => {
168+
result.current[1].push('b');
169+
rerender();
170+
});
171+
172+
expect(originalRef).not.toBe(result.current[0]);
173+
});
174+
175+
it('should maintain stable actions reference after list changes', async () => {
176+
const { result, rerender } = await renderHookSSR(() => useList<string>());
177+
const [, originalActions] = result.current;
178+
179+
expect(result.current[1]).toBe(originalActions);
180+
181+
await act(async () => {
182+
originalActions.push('a');
183+
rerender();
184+
});
185+
186+
expect(result.current[1]).toBe(originalActions);
187+
});
188+
});

0 commit comments

Comments
 (0)