Skip to content

Instantly share code, notes, and snippets.

@bone-house
Created March 20, 2023 14:59
Show Gist options
  • Save bone-house/922e8061d9424f44583d6d8a2b9e7ea3 to your computer and use it in GitHub Desktop.
Save bone-house/922e8061d9424f44583d6d8a2b9e7ea3 to your computer and use it in GitHub Desktop.
View transitions with R3F + spring + react-router-dom
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
}
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