Created
March 20, 2023 14:59
-
-
Save bone-house/922e8061d9424f44583d6d8a2b9e7ea3 to your computer and use it in GitHub Desktop.
View transitions with R3F + spring + react-router-dom
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
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' | |
import { useLocation } from 'react-router-dom' | |
export interface ViewProps { | |
// If the view has animations that should finish before route change happens | |
delayedTransition?: boolean | |
} | |
export interface ViewContext { | |
// If the browser's path is the same as context's path | |
active: boolean | |
path: string | |
updateRoute: () => void | |
viewProps?: ViewProps | |
} | |
export const ViewContext = createContext<[ViewContext, React.Dispatch<ViewProps | undefined>]>(null!) | |
export function ViewProvider({ children }: {children: any}) { | |
const location = useLocation() | |
const [path, setPath] = useState(location.pathname) | |
const [viewProps, setProps] = useState<ViewProps>() | |
const nextPath = useRef(location.pathname) | |
const updateRoute = (path = nextPath.current) => { | |
setPath(path) | |
} | |
const context = useMemo<ViewContext>(() => ({ | |
active: path === location.pathname, | |
path, | |
updateRoute, | |
viewProps, | |
}), [location, viewProps, path]) | |
useEffect(() => { | |
if (!context.viewProps?.delayedTransition) { | |
// Immediately change route | |
updateRoute(location.pathname) | |
} else { | |
// Wait for updateRoute() to be called by View component | |
nextPath.current = location.pathname | |
} | |
}, [context.viewProps?.delayedTransition, location.pathname]) | |
return ( | |
<ViewContext.Provider value={[context, setProps]}> | |
{children} | |
</ViewContext.Provider> | |
) | |
} | |
export function useView() { | |
const location = useLocation() | |
const [context] = useContext(ViewContext) | |
const active = context.active | |
const updateRoute = context.updateRoute | |
return useMemo(() => ({ | |
path: context.path, | |
updateRoute, | |
active, | |
}), [context.path, location.pathname, updateRoute, active]) | |
} | |
// Views that have a transition should be wrapped in this | |
export function View({ children, ...viewProps }: {children: any} & ViewProps) { | |
const [_, setProps] = useContext(ViewContext) | |
useEffect(() => { | |
setProps(viewProps) | |
return () => setProps(undefined) | |
}, [viewProps.delayedTransition]) | |
return children | |
} |
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
import React, { useEffect } from 'react' | |
import { a, config, useTransition } from '@react-spring/three' | |
import { useNavigate, Route, Routes } from 'react-router-dom' | |
import { useView, View } from './ViewContext' | |
import { NotFound } from './NotFound' | |
const dashboardOptions = [ | |
{ | |
to: '/404', | |
}, | |
{ | |
to: '/', | |
}, | |
{ | |
to: '/test', | |
}, | |
] | |
export function TestView() { | |
const view = useView() | |
const navigate = useNavigate() | |
const [transition, transApi] = useTransition(view.active ? dashboardOptions : [], () => ({ | |
trail: Math.max(50, 250 / dashboardOptions.length), | |
from: { scale: 0 }, | |
enter: { scale: 1, config: config.stiff }, | |
leave: { | |
config: config.stiff, | |
scale: 0, | |
onRest: (_, __, c) => { | |
// Switch route when the last item has finished | |
// IDK if theres a better way to do this | |
if (dashboardOptions.indexOf(c) === dashboardOptions.length - 1) { | |
view.updateRoute() | |
} | |
}, | |
}, | |
}), [view.active]) | |
useEffect(() => { | |
transApi.start() | |
}, [view.active]) | |
return ( | |
<View delayedTransition> | |
{transition((props, option, _, i) => { | |
const x = i | |
return ( | |
<a.mesh | |
key={i} | |
position={[x, 0, 0]} | |
onClick={() => navigate(option.to)} | |
scale={props.scale.to((x) => [x, x, x])} | |
> | |
<boxGeometry /> | |
<meshNormalMaterial /> | |
</a.mesh> | |
) | |
})} | |
</View> | |
) | |
} | |
export function Views() { | |
const { path } = useView() | |
return ( | |
<Routes location={path}> | |
<Route path="/" element={<TestView />} /> | |
<Route path="*" element={<NotFound />} /> | |
</Routes> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment