Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-lions-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-simplikit': patch
---

feat(core/hooks): add 'useSet' hook
1 change: 1 addition & 0 deletions packages/core/src/hooks/useSet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useSet } from './useSet.ts';
83 changes: 83 additions & 0 deletions packages/core/src/hooks/useSet/ko/useSet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# useSet

리액트 훅으로, 상태로 Set을 관리해요. 효율적인 상태 관리를 제공하고 안정적인 액션 함수를 제공해요.

## 인터페이스

```ts
function useSet<T>(
initialState?: SetOrValues<T>
): [Omit<Set<T>, 'add' | 'clear' | 'delete'>, SetActions<T>];
```

### 파라미터

<Interface
name="initialState"
type="Set<T> | T[]"
description="초기 Set 상태 (Set 객체 또는 값의 배열). 기본값은 빈 Set이에요."
/>

### 반환 값

<Interface
name=""
type="[Set, SetActions]"
description="Set 상태와 이를 조작하는 액션을 포함한 튜플이에요."
:nested="[
{
name: '[0]',
type: 'Omit<Set<T>, \"add\" | \"clear\" | \"delete\">',
required: false,
description: '변경 메서드가 숨겨진 현재 Set 상태예요.',
},
{
name: '[1].add',
type: '(value: T) => void',
required: false,
description: 'Set에 값을 추가해요.',
},
{
name: '[1].remove',
type: '(value: T) => void',
required: false,
description: 'Set에서 값을 제거해요.',
},
{
name: '[1].toggle',
type: '(value: T) => void',
required: false,
description: '값이 없으면 추가하고, 있으면 제거해요.',
},
{
name: '[1].setAll',
type: '(values: Set<T> | T[]) => void',
required: false,
description: 'Set의 모든 값을 교체해요.',
},
{
name: '[1].reset',
type: '() => void',
required: false,
description: 'Set을 초기 상태로 리셋해요.',
},
]"
/>

## 예시

```tsx
function TagSelector() {
const [selectedTags, { add, remove, toggle }] = useSet<string>(['react']);

return (
<div>
{['react', 'vue', 'svelte'].map(tag => (
<button key={tag} onClick={() => toggle(tag)}>
{selectedTags.has(tag) ? '✓' : ''} {tag}
</button>
))}
</div>
);
}
```
83 changes: 83 additions & 0 deletions packages/core/src/hooks/useSet/useSet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# useSet

A React hook that manages a Set as state. Provides efficient state management and stable action functions.

## Interface

```ts
function useSet<T>(
initialState?: SetOrValues<T>
): [Omit<Set<T>, 'add' | 'clear' | 'delete'>, SetActions<T>];
```

### Parameters

<Interface
name="initialState"
type="Set<T> | T[]"
description="Initial Set state (Set object or array of values). Defaults to an empty Set."
/>

### Return Value

<Interface
name=""
type="[Set, SetActions]"
description="A tuple containing the Set state and actions to manipulate it."
:nested="[
{
name: '[0]',
type: 'Omit<Set<T>, \"add\" | \"clear\" | \"delete\">',
required: false,
description: 'The current Set state with mutation methods hidden.',
},
{
name: '[1].add',
type: '(value: T) => void',
required: false,
description: 'Adds a value to the set.',
},
{
name: '[1].remove',
type: '(value: T) => void',
required: false,
description: 'Removes a value from the set.',
},
{
name: '[1].toggle',
type: '(value: T) => void',
required: false,
description: 'Adds the value if absent, removes it if present.',
},
{
name: '[1].setAll',
type: '(values: Set<T> | T[]) => void',
required: false,
description: 'Replaces all values in the set.',
},
{
name: '[1].reset',
type: '() => void',
required: false,
description: 'Resets the set to its initial state.',
},
]"
/>

## Example

```tsx
function TagSelector() {
const [selectedTags, { add, remove, toggle }] = useSet<string>(['react']);

return (
<div>
{['react', 'vue', 'svelte'].map(tag => (
<button key={tag} onClick={() => toggle(tag)}>
{selectedTags.has(tag) ? '✓' : ''} {tag}
</button>
))}
</div>
);
}
```
218 changes: 218 additions & 0 deletions packages/core/src/hooks/useSet/useSet.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { act } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { renderHookSSR } from '../../_internal/test-utils/renderHookSSR.tsx';

import { useSet } from './useSet.ts';

describe('useSet', () => {
it('is safe on server side rendering', () => {
const initialSet = new Set([1, 2, 3]);
renderHookSSR.serverOnly(() => useSet(initialSet));
});

it('should initialize with a Set', async () => {
const initialSet = new Set(['a', 'b']);
const { result } = await renderHookSSR(() => useSet(initialSet));

expect(result.current[0].has('a')).toBe(true);
expect(result.current[0].has('b')).toBe(true);
expect(result.current[0].size).toBe(2);
});

it('should initialize with an array of values', async () => {
const { result } = await renderHookSSR(() => useSet([1, 2, 3]));

expect(result.current[0].has(1)).toBe(true);
expect(result.current[0].has(2)).toBe(true);
expect(result.current[0].has(3)).toBe(true);
expect(result.current[0].size).toBe(3);
});

it('should initialize with an empty Set when no arguments provided', async () => {
const { result } = await renderHookSSR(() => useSet());

expect(result.current[0].size).toBe(0);
});

it('should add a value to the Set', async () => {
const { result, rerender } = await renderHookSSR(() => useSet<string>());
const [, actions] = result.current;

expect(result.current[0].has('a')).toBe(false);

await act(async () => {
actions.add('a');
rerender();
});

expect(result.current[0].has('a')).toBe(true);
expect(result.current[0].size).toBe(1);
});

it('should not duplicate values when adding an existing value', async () => {
const { result, rerender } = await renderHookSSR(() => useSet(['a']));
const [, actions] = result.current;

await act(async () => {
actions.add('a');
rerender();
});

expect(result.current[0].size).toBe(1);
});

it('should remove a value from the Set', async () => {
const { result, rerender } = await renderHookSSR(() => useSet(['a', 'b']));
const [, actions] = result.current;

await act(async () => {
actions.remove('a');
rerender();
});

expect(result.current[0].has('a')).toBe(false);
expect(result.current[0].has('b')).toBe(true);
expect(result.current[0].size).toBe(1);
});

it('should do nothing when removing a value not in the Set', async () => {
const { result, rerender } = await renderHookSSR(() => useSet(['a']));
const [, actions] = result.current;

await act(async () => {
actions.remove('z');
rerender();
});

expect(result.current[0].has('a')).toBe(true);
expect(result.current[0].size).toBe(1);
});

it('should toggle a value - add if absent', async () => {
const { result, rerender } = await renderHookSSR(() => useSet<string>());
const [, actions] = result.current;

await act(async () => {
actions.toggle('a');
rerender();
});

expect(result.current[0].has('a')).toBe(true);
});

it('should toggle a value - remove if present', async () => {
const { result, rerender } = await renderHookSSR(() => useSet(['a']));
const [, actions] = result.current;

await act(async () => {
actions.toggle('a');
rerender();
});

expect(result.current[0].has('a')).toBe(false);
});

it('should replace all values with setAll', async () => {
const { result, rerender } = await renderHookSSR(() => useSet([1, 2, 3]));
const [, actions] = result.current;

await act(async () => {
actions.setAll([4, 5]);
rerender();
});

expect(result.current[0].has(1)).toBe(false);
expect(result.current[0].has(4)).toBe(true);
expect(result.current[0].has(5)).toBe(true);
expect(result.current[0].size).toBe(2);
});

it('should replace all values with setAll using a Set', async () => {
const { result, rerender } = await renderHookSSR(() => useSet([1, 2, 3]));
const [, actions] = result.current;

await act(async () => {
actions.setAll(new Set([4, 5]));
rerender();
});

expect(result.current[0].has(1)).toBe(false);
expect(result.current[0].has(4)).toBe(true);
expect(result.current[0].has(5)).toBe(true);
expect(result.current[0].size).toBe(2);
});

it('should reset the Set to its initial state', async () => {
const initialSet = new Set([1, 2]);
const { result, rerender } = await renderHookSSR(() => useSet(initialSet));
const [, actions] = result.current;

await act(async () => {
actions.add(3);
actions.remove(1);
rerender();
});

expect(result.current[0].has(1)).toBe(false);
expect(result.current[0].has(3)).toBe(true);

await act(async () => {
actions.reset();
rerender();
});

expect(result.current[0].has(1)).toBe(true);
expect(result.current[0].has(2)).toBe(true);
expect(result.current[0].has(3)).toBe(false);
expect(result.current[0].size).toBe(2);
});

it('should reset to empty Set when initialized with empty Set', async () => {
const { result, rerender } = await renderHookSSR(() => useSet<number>());
const [, actions] = result.current;

await act(async () => {
actions.add(1);
actions.add(2);
rerender();
});

expect(result.current[0].size).toBe(2);

await act(async () => {
actions.reset();
rerender();
});

expect(result.current[0].size).toBe(0);
});

it('should create a new Set reference when values change', async () => {
const { result, rerender } = await renderHookSSR(() => useSet<number>());
const [originalSetRef, actions] = result.current;

await act(async () => {
actions.add(1);
rerender();
});

expect(originalSetRef).not.toBe(result.current[0]);
expect(originalSetRef.has(1)).toBe(false);
expect(result.current[0].has(1)).toBe(true);
});

it('should maintain stable actions reference after Set changes', async () => {
const { result, rerender } = await renderHookSSR(() => useSet<number>());
const [, originalActionsRef] = result.current;

expect(result.current[1]).toBe(originalActionsRef);

await act(async () => {
originalActionsRef.add(1);
rerender();
});

expect(result.current[1]).toBe(originalActionsRef);
});
});
Loading
Loading