このコミットが含まれているのは:
2026-07-18 20:03:26 +09:00
コミット 8f66ee8059
2個のファイルの変更59行の追加58行の削除
+37 -37
ファイルの表示
@@ -155,7 +155,7 @@ const PostDetailRoute = ({ user }: { user: User | null }) => {
} }
const App: FC = () => { const RoutedApp: FC = () => {
const [user, setUser] = useState<User | null> (null) const [user, setUser] = useState<User | null> (null)
const [status, setStatus] = useState (200) const [status, setStatus] = useState (200)
const behaviourSettings = useClientBehaviourSettings () const behaviourSettings = useClientBehaviourSettings ()
@@ -172,41 +172,6 @@ const App: FC = () => {
), ),
[animationMode], [animationMode],
) )
const router = useMemo (
() => createBrowserRouter ([{
path: '*',
element: (
<DialogueProvider>
<UnsavedChangesGuardProvider>
<KeyboardShortcutsProvider>
<MotionConfig
reducedMotion={
animationMode === 'normal'
? 'never'
: animationMode === 'reduced'
? 'user'
: 'always'
}>
<LayoutWrapper>
<motion.div
layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: appLayoutTransition }}
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
<TopNav user={user}/>
<RouteTransitionWrapper
animationMode={animationMode}
user={user}
setUser={setUser}/>
</motion.div>
</LayoutWrapper>
</MotionConfig>
<Toaster/>
</KeyboardShortcutsProvider>
</UnsavedChangesGuardProvider>
</DialogueProvider>) }]),
[LayoutWrapper, animationMode, appLayoutTransition, setUser, user],
)
useEffect (() => { useEffect (() => {
applyClientAppearance () applyClientAppearance ()
@@ -289,11 +254,46 @@ const App: FC = () => {
} }
return ( return (
<DialogueProvider>
<UnsavedChangesGuardProvider>
<KeyboardShortcutsProvider>
<MotionConfig
reducedMotion={
animationMode === 'normal'
? 'never'
: animationMode === 'reduced'
? 'user'
: 'always'
}>
<LayoutWrapper>
<motion.div
layout={animationMode === 'off' ? false : 'position'}
transition={{ layout: appLayoutTransition }}
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
<TopNav user={user}/>
<RouteTransitionWrapper
animationMode={animationMode}
user={user}
setUser={setUser}/>
</motion.div>
</LayoutWrapper>
</MotionConfig>
<Toaster/>
</KeyboardShortcutsProvider>
</UnsavedChangesGuardProvider>
</DialogueProvider>)
}
const router = createBrowserRouter ([{
path: '*',
element: <RoutedApp/> }])
const App: FC = () => (
<> <>
<RouteBlockerOverlay/> <RouteBlockerOverlay/>
{import.meta.env.DEV && <DevModeWatermark/>} {import.meta.env.DEV && <DevModeWatermark/>}
<RouterProvider router={router}/> <RouterProvider router={router}/>
</>) </>)
}
export default App export default App
+18 -17
ファイルの表示
@@ -7,7 +7,7 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { useBlocker, useLocation } from 'react-router-dom' import { useBlocker } from 'react-router-dom'
import { useDialogue } from '@/lib/dialogues/useDialogue' import { useDialogue } from '@/lib/dialogues/useDialogue'
@@ -34,7 +34,6 @@ const UnsavedChangesGuardContext =
export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children }) => { export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children }) => {
const dialogue = useDialogue () const dialogue = useDialogue ()
const location = useLocation ()
const sourcesRef = useRef (new Map<symbol, UnsavedChangesSource> ()) const sourcesRef = useRef (new Map<symbol, UnsavedChangesSource> ())
const bypassNextNavigationRef = useRef (false) const bypassNextNavigationRef = useRef (false)
const [revision, setRevision] = useState (0) const [revision, setRevision] = useState (0)
@@ -68,12 +67,19 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
[sources], [sources],
) )
const hasUnsavedChanges = dirtySources.length > 0 const hasUnsavedChanges = dirtySources.length > 0
const blocker = useBlocker ( const shouldBlock = useCallback (() => {
useCallback ( if (!(hasUnsavedChanges))
() => hasUnsavedChanges && !(bypassNextNavigationRef.current), return false
[hasUnsavedChanges],
), if (bypassNextNavigationRef.current)
) {
bypassNextNavigationRef.current = false
return false
}
return true
}, [hasUnsavedChanges])
const blocker = useBlocker (shouldBlock)
const discardDirtyChanges = useCallback (async (): Promise<boolean> => { const discardDirtyChanges = useCallback (async (): Promise<boolean> => {
if (!(hasUnsavedChanges)) if (!(hasUnsavedChanges))
@@ -93,8 +99,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
if (!(confirmed)) if (!(confirmed))
return false return false
bypassNextNavigationRef.current = true
try try
{ {
for (const source of dirtySources) for (const source of dirtySources)
@@ -102,7 +106,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
} }
catch catch
{ {
bypassNextNavigationRef.current = false
return false return false
} }
@@ -139,10 +142,6 @@ export const UnsavedChangesGuardProvider: FC<PropsWithChildren> = ({ children })
} }
}, [blocker, discardDirtyChanges]) }, [blocker, discardDirtyChanges])
useEffect (() => {
bypassNextNavigationRef.current = false
}, [location.hash, location.pathname, location.search])
useEffect (() => { useEffect (() => {
if (!(hasUnsavedChanges)) if (!(hasUnsavedChanges))
return return
@@ -183,14 +182,16 @@ export const useUnsavedChangesGuard = (
if (context == null) if (context == null)
throw new Error ('UnsavedChangesGuardProvider が必要です.') throw new Error ('UnsavedChangesGuardProvider が必要です.')
const { registerUnsavedChangesSource } = context
useEffect (() => { useEffect (() => {
if (options == null) if (options == null)
return return
return context.registerUnsavedChangesSource ({ return registerUnsavedChangesSource ({
dirty: options.dirty, dirty: options.dirty,
discard: options.onDiscard ?? (() => undefined) }) discard: options.onDiscard ?? (() => undefined) })
}, [context, options?.dirty, options?.onDiscard]) }, [options?.dirty, options?.onDiscard, registerUnsavedChangesSource])
return context return context
} }