-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(useStateList): rework useStateList to make it work properly.
- Loading branch information
Showing
2 changed files
with
157 additions
and
126 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,144 +1,162 @@ | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
import { act, renderHook } from '@testing-library/react-hooks'; | ||
import useStateList from '../useStateList'; | ||
|
||
const callNext = hook => { | ||
act(() => { | ||
const { next } = hook.result.current; | ||
next(); | ||
describe('useStateList', () => { | ||
it('should be defined', () => { | ||
expect(useStateList).toBeDefined(); | ||
}); | ||
}; | ||
|
||
const callPrev = hook => { | ||
act(() => { | ||
const { prev } = hook.result.current; | ||
prev(); | ||
}); | ||
}; | ||
function getHook(list: any[] = ['a', 'b', 'c']) { | ||
return renderHook(({ states }) => useStateList(states), { initialProps: { states: list } }); | ||
} | ||
|
||
describe('happy flow', () => { | ||
const hook = renderHook(({ stateSet }) => useStateList(stateSet), { | ||
initialProps: { | ||
stateSet: ['a', 'b', 'c'], | ||
}, | ||
}); | ||
it('should return an object containing `state`, `next` and `prev`', () => { | ||
const res = getHook().result.current; | ||
|
||
it('should return the first state on initial render', () => { | ||
const { state } = hook.result.current; | ||
expect(state).toBe('a'); | ||
expect(typeof res).toBe('object'); | ||
expect(typeof res.state).toBe('string'); | ||
expect(typeof res.prev).toBe('function'); | ||
expect(typeof res.next).toBe('function'); | ||
}); | ||
|
||
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 on init', () => { | ||
expect(getHook().result.current.state).toBe('a'); | ||
}); | ||
|
||
it('should return the first state again after calling the "next" function "stateSet.length" times', () => { | ||
callNext(hook); | ||
callNext(hook); | ||
describe('next()', () => { | ||
it('should switch states forward and cause re-render', () => { | ||
const hook = getHook(); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('a'); | ||
}); | ||
expect(hook.result.current.state).toBe('a'); | ||
|
||
it('should return the last state again after calling the "prev" function', () => { | ||
callPrev(hook); | ||
act(() => { | ||
hook.result.current.next(); | ||
}); | ||
expect(hook.result.current.state).toBe('b'); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('c'); | ||
}); | ||
act(() => { | ||
hook.result.current.next(); | ||
}); | ||
expect(hook.result.current.state).toBe('c'); | ||
}); | ||
|
||
it('should return the previous state after calling the "prev" function', () => { | ||
callPrev(hook); | ||
it('should on overflow should switch to first element (should be cycled)', () => { | ||
const hook = getHook(); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('b'); | ||
}); | ||
}); | ||
expect(hook.result.current.state).toBe('a'); | ||
|
||
describe('with empty state set', () => { | ||
const hook = renderHook(({ stateSet }) => useStateList(stateSet), { | ||
initialProps: { | ||
stateSet: [], | ||
}, | ||
act(() => { | ||
hook.result.current.next(); | ||
hook.result.current.next(); | ||
hook.result.current.next(); | ||
}); | ||
expect(hook.result.current.state).toBe('a'); | ||
}); | ||
}); | ||
|
||
it('should return undefined on initial render', () => { | ||
const { state } = hook.result.current; | ||
expect(state).toBe(undefined); | ||
}); | ||
describe('prev()', () => { | ||
it('should on overflow should switch to last element (should be cycled)', () => { | ||
const hook = getHook(); | ||
|
||
it('should always return undefined (calling next)', () => { | ||
callNext(hook); | ||
expect(hook.result.current.state).toBe('a'); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe(undefined); | ||
}); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('c'); | ||
}); | ||
|
||
it('should always return undefined (calling prev)', () => { | ||
callPrev(hook); | ||
it('should switch states backward and cause re-render', () => { | ||
const hook = getHook(); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe(undefined); | ||
}); | ||
}); | ||
expect(hook.result.current.state).toBe('a'); | ||
|
||
describe('with a single state set', () => { | ||
const hook = renderHook(({ stateSet }) => useStateList(stateSet), { | ||
initialProps: { | ||
stateSet: ['a'], | ||
}, | ||
}); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('c'); | ||
|
||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('b'); | ||
|
||
it('should return "a" on initial render', () => { | ||
const { state } = hook.result.current; | ||
expect(state).toBe('a'); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('a'); | ||
}); | ||
}); | ||
|
||
it('should always return "a" (calling next)', () => { | ||
callNext(hook); | ||
describe('with empty states list', () => { | ||
it('should have `undefined` state', () => { | ||
expect(getHook([]).result.current.state).toBe(undefined); | ||
}); | ||
|
||
it('should do nothing on next() call', () => { | ||
const hook = getHook([]); | ||
act(() => { | ||
hook.result.current.next(); | ||
}); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('a'); | ||
}); | ||
expect(hook.result.current.state).toBe(undefined); | ||
}); | ||
|
||
it('should always return "a" (calling prev)', () => { | ||
callPrev(hook); | ||
it('should do nothing on prev() call', () => { | ||
const hook = getHook([]); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('a'); | ||
expect(hook.result.current.state).toBe(undefined); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('with stateSet updates', () => { | ||
const hook = renderHook(({ stateSet }) => useStateList(stateSet), { | ||
initialProps: { | ||
stateSet: ['a', 'c', 'b', 'f', 'g'], | ||
}, | ||
}); | ||
describe('on state list shrink', () => { | ||
it('should set last element as state if index was beyond new last element', () => { | ||
const hook = getHook(); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('c'); | ||
|
||
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 | ||
hook.rerender({ states: ['a', 'b'] }); | ||
|
||
// Update the state set with less elements | ||
hook.rerender({ | ||
stateSet: ['a', 'c'], | ||
expect(hook.result.current.state).toBe('b'); | ||
}); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('c'); | ||
it('should so nothing if current index within new range', () => { | ||
const hook = getHook(); | ||
act(() => { | ||
hook.result.current.prev(); | ||
}); | ||
expect(hook.result.current.state).toBe('c'); | ||
|
||
hook.rerender({ states: ['a', 'b', 'c', 'd'] }); | ||
|
||
expect(hook.result.current.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'], | ||
describe('ou unmounted component', () => { | ||
it('next() should not do anything', () => { | ||
const hook = getHook(); | ||
const { next } = hook.result.current; | ||
|
||
hook.unmount(); | ||
act(() => next()); | ||
|
||
expect(hook.result.current.state).toBe('a'); | ||
}); | ||
|
||
const { state } = hook.result.current; | ||
expect(state).toBe('f'); | ||
it('prev() should not do anything', () => { | ||
const hook = getHook(); | ||
const { prev } = hook.result.current; | ||
|
||
hook.unmount(); | ||
act(() => prev()); | ||
|
||
expect(hook.result.current.state).toBe('a'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,46 @@ | ||
import { useState, useCallback } from 'react'; | ||
|
||
import { useCallback, useRef } from 'react'; | ||
import useMountedState from './useMountedState'; | ||
import useUpdate from './useUpdate'; | ||
import useUpdateEffect from './useUpdateEffect'; | ||
|
||
export default function useStateList<T>(stateSet: T[] = []): { state: T; next: () => void; prev: () => void } { | ||
const [currentIndex, setCurrentIndex] = useState(0); | ||
const isMounted = useMountedState(); | ||
const update = useUpdate(); | ||
const index = useRef(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. | ||
// If new state list is shorter that before - switch to the last element | ||
useUpdateEffect(() => { | ||
if (!stateSet[currentIndex]) { | ||
setCurrentIndex(stateSet.length - 1); | ||
if (stateSet.length <= index.current) { | ||
index.current = stateSet.length - 1; | ||
update(); | ||
} | ||
}, [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]); | ||
}, [stateSet.length]); | ||
|
||
return { | ||
state: stateSet[currentIndex], | ||
next, | ||
prev, | ||
state: stateSet[index.current], | ||
next: useCallback(() => { | ||
// do nothing on unmounted component | ||
if (!isMounted()) { | ||
return; | ||
} | ||
|
||
// act only if stateSet has element within | ||
if (stateSet.length) { | ||
index.current = (index.current + 1) % stateSet.length; | ||
update(); | ||
} | ||
}, [stateSet, index]), | ||
prev: useCallback(() => { | ||
// do nothing on unmounted component | ||
if (!isMounted()) { | ||
return; | ||
} | ||
|
||
// act only if stateSet has element within | ||
if (stateSet.length) { | ||
index.current = index.current - 1 < 0 ? stateSet.length - 1 : index.current - 1; | ||
update(); | ||
} | ||
}, [stateSet, index]), | ||
}; | ||
} |