Skip to content

Commit

Permalink
feat: add useSwitchTransition
Browse files Browse the repository at this point in the history
complete #3
fix #1
  • Loading branch information
iamyoki committed Dec 22, 2021
1 parent 6b2fda1 commit 262e720
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/helpers/setAnimationFrameTimeout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Canceller = {
export type Canceller = {
id?: number;
};

Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
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';
1 change: 0 additions & 1 deletion src/useSwitchTransition/index.ts

This file was deleted.

37 changes: 37 additions & 0 deletions src/useSwitchTransition/index.tsx
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;
}
18 changes: 18 additions & 0 deletions src/useSwitchTransition/types.ts
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>[]>>;
};
51 changes: 51 additions & 0 deletions src/useSwitchTransition/useDefaultMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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);
console.log(lastItem.state, state, 'default');
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]);
}
77 changes: 77 additions & 0 deletions src/useSwitchTransition/useInOutMode.ts
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]);
}
51 changes: 51 additions & 0 deletions src/useSwitchTransition/useOutInMode.ts
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]);
}

0 comments on commit 262e720

Please sign in to comment.