Skip to content
Open
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
14 changes: 11 additions & 3 deletions docs/useStateList.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `useStateList`

Provides handles for circular iteration over states list.
Supports forward and backward iterations and arbitrary position set.
Supports forward and backward iterations and arbitrary position set , and now supports initializing with default index.

## Usage

Expand All @@ -10,9 +10,10 @@ import { useStateList } from 'react-use';
import { useRef } from 'react';

const stateSet = ['first', 'second', 'third', 'fourth', 'fifth'];
const defaultCurrentIndex = 2 ; // Start at 'third'

const Demo = () => {
const { state, prev, next, setStateAt, setState, currentIndex, isFirst, isLast } = useStateList(stateSet);
const { state, prev, next, setStateAt, setState, currentIndex, isFirst, isLast } = useStateList(stateSet , defaultCurrentIndex);
const indexInput = useRef<HTMLInputElement>(null);
const stateInput = useRef<HTMLInputElement>(null);

Expand All @@ -38,11 +39,18 @@ const Demo = () => {
## Reference

```ts
const { state, currentIndex, prev, next, setStateAt, setState, isFirst, isLast } = useStateList<T>(stateSet: T[] = []);
const { state, currentIndex, prev, next, setStateAt, setState, isFirst, isLast } = useStateList<T>(stateSet: T[] = [] , defaultCurrentIndex?: number);
```

### Parameters

- **`stateSet`**_`: T[]`_ — List of possible states.
- **`defaultCurrentIndex`**_`: number`_ (optional) — The index to start from. Defaults to `0`. If out of bounds, it will be clamped to the valid range.

If `stateSet` changed, became shorter than before and `currentIndex` left in shrunk gap - the last element of list will be taken as current.

### Returns

- **`state`**_`: T`_ &mdash; current state value;
- **`currentIndex`**_`: number`_ &mdash; current state index;
- **`prev()`**_`: void`_ &mdash; switches state to the previous one. If first element selected it will switch to the last one;
Expand Down
34 changes: 16 additions & 18 deletions src/useStateList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@ export interface UseStateListReturn<T> {
isLast: boolean;
}

export default function useStateList<T>(stateSet: T[] = []): UseStateListReturn<T> {
// Add defaultCurrentIndex parameter
export default function useStateList<T>(
stateSet: T[] = [],
defaultCurrentIndex: number = 0
): UseStateListReturn<T> {
const isMounted = useMountedState();
const update = useUpdate();
const index = useRef(0);
// Initialize index with defaultCurrentIndex, clamp to valid range
const initialIndex =
stateSet.length === 0
? 0
: Math.max(0, Math.min(defaultCurrentIndex, stateSet.length - 1));
const index = useRef(initialIndex);

// If new state list is shorter that before - switch to the last element
// If new state list is shorter than before - switch to the last element
useUpdateEffect(() => {
if (stateSet.length <= index.current) {
index.current = stateSet.length - 1;
Expand All @@ -32,34 +41,23 @@ export default function useStateList<T>(stateSet: T[] = []): UseStateListReturn<
next: () => actions.setStateAt(index.current + 1),
prev: () => actions.setStateAt(index.current - 1),
setStateAt: (newIndex: number) => {
// do nothing on unmounted component
if (!isMounted()) return;

// do nothing on empty states list
if (!stateSet.length) return;

// in case new index is equal current - do nothing
if (newIndex === index.current) return;

// it gives the ability to travel through the left and right borders.
// 4ex: if list contains 5 elements, attempt to set index 9 will bring use to 5th element
// in case of negative index it will start counting from the right, so -17 will bring us to 4th element
index.current =
newIndex >= 0
? newIndex % stateSet.length
: stateSet.length + (newIndex % stateSet.length);
update();
},
setState: (state: T) => {
// do nothing on unmounted component
if (!isMounted()) return;

const newIndex = stateSet.length ? stateSet.indexOf(state) : -1;

if (newIndex === -1) {
throw new Error(`State '${state}' is not a valid state (does not exist in state list)`);
throw new Error(
`State '${state}' is not a valid state (does not exist in state list)`
);
}

index.current = newIndex;
update();
},
Expand All @@ -74,4 +72,4 @@ export default function useStateList<T>(stateSet: T[] = []): UseStateListReturn<
isLast: index.current === stateSet.length - 1,
...actions,
};
}
}
22 changes: 19 additions & 3 deletions tests/useStateList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ describe('useStateList', () => {
expect(useStateList).toBeDefined();
});

function getHook(list: any[] = ['a', 'b', 'c']) {
return renderHook(({ states }) => useStateList(states), { initialProps: { states: list } });
}
function getHook(list: any[] = ['a', 'b', 'c'], defaultCurrentIndex?: number) {
return renderHook(
({ states, index }) =>
typeof index === 'number'
? useStateList(states, index)
: useStateList(states),
{
initialProps: defaultCurrentIndex !== undefined
? { states: list, index: defaultCurrentIndex }
: { states: list }
}
);
}

it('should return an object containing `state`, `next` and `prev`', () => {
const res = getHook().result.current;
Expand Down Expand Up @@ -40,6 +50,12 @@ describe('useStateList', () => {
expect(hook.result.current.isLast).toBe(true);
});

it('should initialize with the provided default index', () => {
const hook = getHook(['a', 'b', 'c'], 1);
expect(hook.result.current.state).toBe('b');
expect(hook.result.current.currentIndex).toBe(1);
});

describe('setState()', () => {
it('should set state value if it exists in states list', () => {
const hook = getHook();
Expand Down