-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from iamyoki/feature
Feature add useSwitchTransition hook and several optimization
- Loading branch information
Showing
9 changed files
with
276 additions
and
7 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
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 |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export type Canceller = { | ||
id?: number; | ||
}; | ||
|
||
export function setAnimationFrameTimeout( | ||
callback: Function, | ||
timeout: number = 0 | ||
) { | ||
const startTime = performance.now(); | ||
const canceller: Canceller = {}; | ||
|
||
function call() { | ||
canceller.id = requestAnimationFrame((now) => { | ||
if (now - startTime > timeout) { | ||
callback(); | ||
} else { | ||
call(); | ||
} | ||
}); | ||
} | ||
|
||
call(); | ||
return canceller; | ||
} | ||
|
||
export function clearAnimationFrameTimeout(canceller: Canceller) { | ||
if (canceller.id) cancelAnimationFrame(canceller.id); | ||
} |
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,2 +1,3 @@ | ||
export {useTransition} from './useTransition'; | ||
export {Transition} from './Transition'; | ||
export * from './useTransition'; | ||
export * from './useSwitchTransition'; | ||
export * from './Transition'; |
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 |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {Fragment, useRef, useState} from 'react'; | ||
import {Stage} from '../useTransition'; | ||
import {ListItem, Mode} from './types'; | ||
import {useDefaultMode} from './useDefaultMode'; | ||
import {useInOutMode} from './useInOutMode'; | ||
import {useOutInMode} from './useOutInMode'; | ||
|
||
type RenderCallback<S> = (state: S, stage: Stage) => React.ReactNode; | ||
|
||
export function useSwitchTransition<S>(state: S, timeout: number, mode?: Mode) { | ||
const keyRef = useRef(0); | ||
const firstDefaultItem: ListItem<S> = { | ||
state, | ||
key: keyRef.current, | ||
stage: 'enter', | ||
}; | ||
const [list, setList] = useState([firstDefaultItem]); | ||
|
||
// for default mode only | ||
useDefaultMode({state, timeout, keyRef, mode, list, setList}); | ||
|
||
// for out-in mode only | ||
useOutInMode({state, timeout, keyRef, mode, list, setList}); | ||
|
||
// for in-out mode only | ||
useInOutMode({state, timeout, keyRef, mode, list, setList}); | ||
|
||
function transition(renderCallback: RenderCallback<S>) { | ||
return list.map((item) => ( | ||
<Fragment key={item.key}> | ||
{renderCallback(item.state, item.stage)} | ||
</Fragment> | ||
)); | ||
} | ||
|
||
return transition; | ||
} |
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 |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import {Stage} from '../useTransition'; | ||
|
||
export type Mode = 'default' | 'out-in' | 'in-out'; | ||
|
||
export type ListItem<S> = { | ||
state: S; | ||
key: number; | ||
stage: Stage; | ||
}; | ||
|
||
export type ModeHookParam<S = any> = { | ||
state: S; | ||
timeout: number; | ||
mode?: Mode; | ||
keyRef: React.MutableRefObject<number>; | ||
list: ListItem<S>[]; | ||
setList: React.Dispatch<React.SetStateAction<ListItem<S>[]>>; | ||
}; |
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 |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import {useEffect} from 'react'; | ||
import {ListItem, ModeHookParam} from './types'; | ||
|
||
export function useDefaultMode<S>({ | ||
state, | ||
timeout, | ||
mode, | ||
keyRef, | ||
list, | ||
setList, | ||
}: ModeHookParam<S>) { | ||
useEffect(() => { | ||
// skip unmatched mode 🚫 | ||
if (mode !== undefined && mode !== 'default') return; | ||
|
||
// skip fist mount and any unchanged effect 🚫 | ||
const [lastItem] = list.slice(-1); | ||
if (lastItem.state === state) return; | ||
|
||
// 0 update key | ||
const prevKey = keyRef.current; // save prev key | ||
keyRef.current++; // update to last item key | ||
const curKey = keyRef.current; // save cur key (for async gets) | ||
|
||
// 1 add new item immediately with stage 'from' | ||
setList((prev) => prev.concat({state, key: curKey, stage: 'from'})); | ||
|
||
// 1.1 change this item immediately with stage 'enter' | ||
const isCurItem = (item: ListItem<S>) => item.key === curKey; | ||
setTimeout(() => { | ||
setList((prev) => | ||
prev.map((item) => (isCurItem(item) ? {...item, stage: 'enter'} : item)) | ||
); | ||
}); | ||
|
||
// 1.2 leave prev item immediately with stage 'leave' | ||
const shouldItemLeave = (item: ListItem<S>) => item.key === prevKey; | ||
setList((prev) => | ||
prev.map((item) => | ||
shouldItemLeave(item) ? {...item, stage: 'leave'} : item | ||
) | ||
); | ||
|
||
// 2 unmount leaved item after timeout | ||
const shouldMountItem = (item: ListItem<S>) => item.key !== prevKey; | ||
setTimeout(() => { | ||
setList((prev) => prev.filter(shouldMountItem)); | ||
}, timeout); | ||
}, [keyRef, list, mode, setList, state, timeout]); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import {useEffect, useRef} from 'react'; | ||
import { | ||
Canceller, | ||
clearAnimationFrameTimeout, | ||
setAnimationFrameTimeout, | ||
} from '../helpers/setAnimationFrameTimeout'; | ||
import {ModeHookParam} from './types'; | ||
|
||
export function useInOutMode<S>({ | ||
state, | ||
timeout, | ||
mode, | ||
keyRef, | ||
list, | ||
setList, | ||
}: ModeHookParam<S>) { | ||
const timerRef = useRef<Canceller>({}); | ||
const timerRef2 = useRef<Canceller>({}); | ||
|
||
useEffect(() => { | ||
// skip unmatched mode 🚫 | ||
if (mode !== 'in-out') return; | ||
|
||
const [lastItem, secondLastItem] = list.reverse(); | ||
|
||
// if state has changed && stage is enter (add new item) | ||
if (lastItem.state !== state && lastItem.stage === 'enter') { | ||
// 1 add new item with stage 'from' | ||
keyRef.current++; | ||
setList((prev) => | ||
prev.slice(-1).concat({state, key: keyRef.current, stage: 'from'}) | ||
); | ||
} | ||
|
||
// if state hasn't changed && stage is from (enter that new item) | ||
if (lastItem.state === state && lastItem.stage === 'from') { | ||
// 2 set that new item's stage to 'enter' immediately | ||
setAnimationFrameTimeout(() => { | ||
setList([secondLastItem, {...lastItem, stage: 'enter'}]); | ||
}); | ||
} | ||
|
||
// if state hasn't changed | ||
// && stage is enter | ||
// && second last item exist | ||
// && second last item enter | ||
// (leave second last item) | ||
if ( | ||
lastItem.state === state && | ||
lastItem.stage === 'enter' && | ||
secondLastItem && | ||
secondLastItem?.stage === 'enter' | ||
) { | ||
// 3 leave second last item after new item enter animation ends | ||
clearAnimationFrameTimeout(timerRef.current); | ||
timerRef.current = setAnimationFrameTimeout(() => { | ||
setList([{...secondLastItem, stage: 'leave'}, lastItem]); | ||
}, timeout); | ||
} | ||
|
||
// if second last item exist | ||
// && second last item is enter | ||
// (unmount second last item) | ||
if (secondLastItem && secondLastItem.stage === 'leave') { | ||
// 4 unmount second last item after it's leave animation ends | ||
clearAnimationFrameTimeout(timerRef2.current); | ||
timerRef2.current = setAnimationFrameTimeout(() => { | ||
setList([lastItem]); | ||
}, timeout); | ||
} | ||
|
||
return () => { | ||
clearAnimationFrameTimeout(timerRef.current); | ||
clearAnimationFrameTimeout(timerRef2.current); | ||
}; | ||
}, [keyRef, list, mode, setList, state, timeout]); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import {useEffect, useRef} from 'react'; | ||
import { | ||
Canceller, | ||
clearAnimationFrameTimeout, | ||
setAnimationFrameTimeout, | ||
} from '../helpers/setAnimationFrameTimeout'; | ||
import {ModeHookParam} from './types'; | ||
|
||
export function useOutInMode<S>({ | ||
state, | ||
timeout, | ||
mode, | ||
keyRef, | ||
list, | ||
setList, | ||
}: ModeHookParam<S>) { | ||
const timerRef = useRef<Canceller>({}); | ||
|
||
useEffect(() => { | ||
// skip unmatched mode 🚫 | ||
if (mode !== 'out-in') return; | ||
|
||
const [lastItem] = list.slice(-1); | ||
|
||
// if state has changed && stage is enter (trigger prev last item to leave) | ||
if (lastItem.state !== state && lastItem.stage === 'enter') { | ||
// 1 leave prev last item | ||
setList([{...lastItem, stage: 'leave'}]); | ||
} | ||
|
||
// if state has changed && stage is leave (add new item after prev last item leave ani ends) | ||
if (lastItem.state !== state && lastItem.stage === 'leave') { | ||
// 2 add new item after prev last item leave animation ends | ||
clearAnimationFrameTimeout(timerRef.current); | ||
timerRef.current = setAnimationFrameTimeout(() => { | ||
keyRef.current++; | ||
setList([{state, key: keyRef.current, stage: 'from'}]); | ||
}, timeout); | ||
} | ||
|
||
// if state hasn't change && stage is from | ||
if (lastItem.state === state && lastItem.stage === 'from') { | ||
// 3 change that new item's stage to 'enter' immediately | ||
setAnimationFrameTimeout(() => { | ||
setList((prev) => [{...prev[0], stage: 'enter'}]); | ||
}); | ||
} | ||
|
||
return () => clearAnimationFrameTimeout(timerRef.current); | ||
}, [keyRef, list, mode, setList, state, timeout]); | ||
} |
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