Skip to content

Commit

Permalink
feat: add useCircularIterate
Browse files Browse the repository at this point in the history
  • Loading branch information
d-asensio committed Aug 25, 2019
1 parent c73e92f commit 8d84340
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
- [`usePrevious`](./docs/usePrevious.md) — returns the previous state or props.
- [`useObservable`](./docs/useObservable.md) — tracks latest value of an `Observable`.
- [`useSetState`](./docs/useSetState.md) — creates `setState` method which works like `this.setState`. [![][img-demo]](https://codesandbox.io/s/n75zqn1xp0)
- [`useStateList`](./docs/useStateList.md) — circularly iterates over an array.
- [`useToggle` and `useBoolean`](./docs/useToggle.md) — tracks state of a boolean.
- [`useCounter` and `useNumber`](./docs/useCounter.md) — tracks state of a number. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usecounter--demo)
- [`useList`](./docs/useList.md) — tracks state of an array.
Expand Down
25 changes: 25 additions & 0 deletions docs/useStateList.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# `useStateList`

React state hook that circularly iterates over an array.

## Usage

```jsx
import { useStateList } from 'react-use';

const stateSet = ['first', 'second', 'third', 'fourth', 'fifth'];

const Demo = () => {
const {state, prev, next} = useStateList(stateSet);

return (
<div>
<pre>{state}</pre>
<button onClick={() => prev()}>prev</button>
<button onClick={() => next()}>next</button>
</div>
);
};
```

> If the `stateSet` is changed by a shorter one the hook will select the last element of it.
22 changes: 22 additions & 0 deletions src/__stories__/useStateList.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useStateList } from '..';
import ShowDocs from './util/ShowDocs';

const stateSet = ['first', 'second', 'third', 'fourth', 'fifth'];

const Demo = () => {
const { state, prev, next } = useStateList(stateSet);

return (
<div>
<pre>{state}</pre>
<button onClick={() => prev()}>prev</button>
<button onClick={() => next()}>next</button>
</div>
);
};

storiesOf('State|useStateList', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useStateList.md')} />)
.add('Demo', () => <Demo />);
144 changes: 144 additions & 0 deletions src/__tests__/useStateList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useStateList from '../useStateList';

const callNext = hook => {
act(() => {
const { next } = hook.result.current;
next();
});
};

const callPrev = hook => {
act(() => {
const { prev } = hook.result.current;
prev();
});
};

describe('happy flow', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a', 'b', 'c'],
},
});

it('should return the first state on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe('a');
});

it('should return the second state after calling the "next" function', () => {
callNext(hook);

const { state } = hook.result.current;
expect(state).toBe('b');
});

it('should return the first state again after calling the "next" function "stateSet.length" times', () => {
callNext(hook);
callNext(hook);

const { state } = hook.result.current;
expect(state).toBe('a');
});

it('should return the last state again after calling the "prev" function', () => {
callPrev(hook);

const { state } = hook.result.current;
expect(state).toBe('c');
});

it('should return the previous state after calling the "prev" function', () => {
callPrev(hook);

const { state } = hook.result.current;
expect(state).toBe('b');
});
});

describe('with empty state set', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: [],
},
});

it('should return undefined on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe(undefined);
});

it('should always return undefined (calling next)', () => {
callNext(hook);

const { state } = hook.result.current;
expect(state).toBe(undefined);
});

it('should always return undefined (calling prev)', () => {
callPrev(hook);

const { state } = hook.result.current;
expect(state).toBe(undefined);
});
});

describe('with a single state set', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a'],
},
});

it('should return "a" on initial render', () => {
const { state } = hook.result.current;
expect(state).toBe('a');
});

it('should always return "a" (calling next)', () => {
callNext(hook);

const { state } = hook.result.current;
expect(state).toBe('a');
});

it('should always return "a" (calling prev)', () => {
callPrev(hook);

const { state } = hook.result.current;
expect(state).toBe('a');
});
});

describe('with stateSet updates', () => {
const hook = renderHook(({ stateSet }) => useStateList(stateSet), {
initialProps: {
stateSet: ['a', 'c', 'b', 'f', 'g'],
},
});

it('should return the last element after updating with a shorter state set', () => {
// Go to the 4th state
callNext(hook); // c
callNext(hook); // b
callNext(hook); // f

// Update the state set with less elements
hook.rerender({
stateSet: ['a', 'c'],
});

const { state } = hook.result.current;
expect(state).toBe('c');
});

it('should return the element in the same position after updating with a larger state set', () => {
hook.rerender({
stateSet: ['a', 'f', 'l'],
});

const { state } = hook.result.current;
expect(state).toBe('f');
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export { default as useSpeech } from './useSpeech';
// not exported because of peer dependency
// export { default as useSpring } from './useSpring';
export { default as useStartTyping } from './useStartTyping';
export { default as useStateList } from './useStateList';
export { default as useThrottle } from './useThrottle';
export { default as useThrottleFn } from './useThrottleFn';
export { default as useTimeout } from './useTimeout';
Expand Down
33 changes: 33 additions & 0 deletions src/useStateList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState, useCallback } from 'react';

import useUpdateEffect from './useUpdateEffect';

export default function useStateList<T>(stateSet: T[] = []): { state: T; next: () => void; prev: () => void } {
const [currentIndex, setCurrentIndex] = useState(0);

// In case we receive a different state set, check if the current index still exists and
// reset it to the last if it don't.
useUpdateEffect(() => {
if (!stateSet[currentIndex]) {
setCurrentIndex(stateSet.length - 1);
}
}, [stateSet]);

const next = useCallback(() => {
const nextStateIndex = stateSet.length === currentIndex + 1 ? 0 : currentIndex + 1;

setCurrentIndex(nextStateIndex);
}, [stateSet, currentIndex]);

const prev = useCallback(() => {
const prevStateIndex = currentIndex === 0 ? stateSet.length - 1 : currentIndex - 1;

setCurrentIndex(prevStateIndex);
}, [stateSet, currentIndex]);

return {
state: stateSet[currentIndex],
next,
prev,
};
}

0 comments on commit 8d84340

Please sign in to comment.