Skip to content

Commit

Permalink
feat: reduce the number of renderings in the default mode of `useSwit…
Browse files Browse the repository at this point in the history
…chTransition`
  • Loading branch information
Daydreamer-riri committed May 10, 2024
1 parent 118c071 commit 25cb686
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 52 deletions.
8 changes: 8 additions & 0 deletions src/helpers/useStateChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useRef } from 'react'

export function useStateChange<S>(state: S) {
const curRef = useRef(state)
const hasChanged = curRef.current !== state
curRef.current = state
return hasChanged
}
15 changes: 10 additions & 5 deletions src/hooks/useSwitchTransition/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import type { TransitionOptions } from '../../types'
import type { StatusState } from '../../status'
import { STATUS, getState } from '../../status'
import type { Timeout } from '../../helpers/getTimeout'
import { useStateChange } from '../../helpers/useStateChange'
import { useDefaultMode } from './useDefaultMode'
import { useOutInMode } from './useOutInMode'
import { useInOutMode } from './useInOutMode'

export type SwitchTransitionOptions = Omit<TransitionOptions, 'onStatusChange' | 'from' | 'enter' | 'exit' | 'initialEntered'> & { mode?: Mode }
export type SwitchTransitionOptions = Omit<TransitionOptions, 'onStatusChange' | 'enter' | 'exit' | 'initialEntered'> & { mode?: Mode }

export type Mode = 'default' | 'out-in' | 'in-out'
export interface ModeHookParam<S> {
state: S
hasChanged: boolean
timeout: Timeout
mode?: Mode
keyRef: React.MutableRefObject<number>
list: ListItem<S>[]
setList: React.Dispatch<React.SetStateAction<ListItem<S>[]>>
from: boolean
}

export type SwitchRenderCallback<S> = (state: S, statusState: StatusState & { prevState?: S, nextState?: S }) => React.ReactNode
Expand All @@ -33,6 +36,7 @@ export function useSwitchTransition<S>(state: S, options?: SwitchTransitionOptio
const {
timeout = 300,
mode = 'default',
from = true,
} = options || {}

const keyRef = useRef(0)
Expand All @@ -42,15 +46,16 @@ export function useSwitchTransition<S>(state: S, options?: SwitchTransitionOptio
...getState(STATUS.entered),
}
const [list, setList] = useState([firstDefaultItem])
const hasChanged = useStateChange(state)

// for default mode only
useDefaultMode({ state, timeout, keyRef, mode, list, setList })
useDefaultMode({ state, timeout, keyRef, mode, list, setList, hasChanged, from })

// for out-in mode only
useOutInMode({ state, timeout, keyRef, mode, list, setList })
useOutInMode({ state, timeout, keyRef, mode, list, setList, hasChanged, from })

// for in-out mode only
useInOutMode({ state, timeout, keyRef, mode, list, setList })
useInOutMode({ state, timeout, keyRef, mode, list, setList, hasChanged, from })

const isResolved = list.every(item => item.isResolved)

Expand All @@ -62,5 +67,5 @@ export function useSwitchTransition<S>(state: S, options?: SwitchTransitionOptio
))
}

return { transition, isResolved }
return { transition, isResolved, list }
}
103 changes: 56 additions & 47 deletions src/hooks/useSwitchTransition/useDefaultMode.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,82 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { STATUS, getState } from '../../status'
import { nextTick } from '../../helpers/setAnimationFrameTimeout'
import { getTimeout } from '../../helpers/getTimeout'
import type { ListItem, ModeHookParam } from './index'

function nowFn(callback: () => unknown) {
return callback()
}

export function useDefaultMode<S>({
state,
timeout,
mode,
keyRef,
list,
setList,
hasChanged,
from,
}: ModeHookParam<S>) {
const { enterTimeout, exitTimeout } = getTimeout(timeout)
const timeoutIdMap = useState(() => new Map<number, number>())[0]
useEffect(() => {
// skip unmatched mode 🚫
if (mode !== undefined && mode !== 'default')
return

const [lastItem] = list.slice(-1)
if (lastItem.state === state)
return
const nextTickOrNow = from ? nextTick : nowFn
// skip unmatched mode 🚫
if (mode !== undefined && mode !== 'default')
return

if (!hasChanged)
return

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)
// 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, prevState: lastItem.state, key: curKey, ...getState(STATUS.from) }))
// 1 add new item immediately with stage 'from'
setList(prev => prev.concat({ state, prevState: lastItem.state, key: curKey, ...getState(STATUS.from) }))

// 1.1 change this item immediately with stage 'entering'
const isCurItem = (item: ListItem<S>) => item.key === curKey
nextTick(() => {
// 1.1 change this item immediately with stage 'entering'
const isCurItem = (item: ListItem<S>) => item.key === curKey
nextTickOrNow(() => {
setList(prev =>
prev.map(item => (isCurItem(item) ? { ...item, ...getState(STATUS.entering) } : item)),
)
const id = window.setTimeout(() => {
setList(prev =>
prev.map(item => (isCurItem(item) ? { ...item, ...getState(STATUS.entering) } : item)),
prev.map(item => (isCurItem(item) ? { ...item, ...getState(STATUS.entered) } : item)),
)
const id = window.setTimeout(() => {
setList(prev =>
prev.map(item => (isCurItem(item) ? { ...item, ...getState(STATUS.entered) } : item)),
)
timeoutIdMap.delete(curKey)
}, enterTimeout)
timeoutIdMap.set(curKey, id)
})
timeoutIdMap.delete(curKey)
}, enterTimeout)
timeoutIdMap.set(curKey, id)
})

// 1.2 leave prev item immediately with stage 'exiting'
const shouldItemLeave = (item: ListItem<S>) => item.key === prevKey
setList(prev =>
prev.map(item => {
if (!shouldItemLeave(item))
return item
// 1.2 leave prev item immediately with stage 'exiting'
const shouldItemLeave = (item: ListItem<S>) => item.key === prevKey
setList(prev =>
prev.map(item => {
if (!shouldItemLeave(item))
return item

const id = timeoutIdMap.get(item.key)
if (id) {
clearTimeout(id)
timeoutIdMap.delete(item.key)
}
const id = timeoutIdMap.get(item.key)
if (id) {
clearTimeout(id)
timeoutIdMap.delete(item.key)
}

return { ...item, nextState: state, ...getState(STATUS.exiting) }
},
),
)
return { ...item, nextState: state, ...getState(STATUS.exiting) }
},
),
)

// 2 unmount leaved item after timeout
const shouldUnmountItem = (item: ListItem<S>) => item.key !== prevKey
setTimeout(() => {
setList(prev => prev.filter(shouldUnmountItem))
}, exitTimeout)
}, [keyRef, list, mode, setList, state, enterTimeout, exitTimeout])
// 2 unmount leaved item after timeout
const shouldUnmountItem = (item: ListItem<S>) => item.key !== prevKey
setTimeout(() => {
setList(prev => prev.filter(shouldUnmountItem))
}, exitTimeout)
}

0 comments on commit 25cb686

Please sign in to comment.