I have an application with nested routes and am hoping to use React Transition Group to transition between the routes, both top level and nested.
My challenge is that I have a context provider wrapping the nested routes. That context provider is unmounting when the page transitions between nested routes, causing the provider to lose state. Without the transitions, the context provider does not unmount and thus state is preserved.
I've implemented a simplified example in a code sandbox: https://codesandbox.io/s/elegant-star-lp7yjx?file=/src/App.js
App
import React from "react";
import {
Navigate,
Route,
Routes,
NavLink,
useLocation
} from "react-router-dom";
import "./styles.css";
import { SwitchTransition, CSSTransition } from "react-transition-group";
const SubPage1 = () => {
return (
<div>
<h1>SubPage1</h1>
<NavLink to="/2">Go to SubPage2</NavLink>
</div>
);
};
const SubPage2 = () => {
return (
<div>
<h1>SubPage2</h1>
<NavLink to="/1">Go to SubPage1</NavLink>
</div>
);
};
const Page1 = () => {
const location = useLocation();
return (
<MyContextProvider>
<h1>Page1</h1>
<NavLink to="/2">Go to Page2</NavLink>
<Routes location={location}>
<Route path="/1" element={<SubPage1 />} />
<Route path="/2" element={<SubPage2 />} />
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
</MyContextProvider>
);
};
const Page2 = () => {
return (
<div>
<h1>Page2</h1>
<NavLink to="/1">Go to Page1</NavLink>
</div>
);
};
const MyContext = React.createContext();
const MyContextProvider = ({ children }) => {
React.useEffect(() => console.log("Context mounting"), []);
return <MyContext.Provider>{children}</MyContext.Provider>;
};
export default function App() {
const location = useLocation();
return (
<SwitchTransition>
<CSSTransition
key={location.key}
classNames="right-to-left"
timeout={200}
>
<Routes>
<Route path="/1" element={<Page1 />} />
<Route path="/2" element={<Page2 />} />
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
</CSSTransition>
</SwitchTransition>
);
}
index.js
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { MemoryRouter } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<MemoryRouter>
<App />
</MemoryRouter>
</StrictMode>
);
Am I doing something wrong or is this just not supported?
The root routes are unable to render the descendent routes because they are missing the trailing "*"
wildcard matcher on their paths.
<Routes>
<Route path="/1" element={<Page1 />} /> // only match "/1" exactly!!
<Route path="/2" element={<Page2 />} /> // only match "/2" exactly!!
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
There's a malformed redirect in the Page1 component rendered on
"/1"that is simply redirecting to
"/1"creating a render loop. Link targets starting with
"/"are ***absolute*** paths. The redirect should redirect to the descendent
"/1/1"` route.
const Page1 = () => {
const location = useLocation();
return (
<MyContextProvider>
<h1>Page1</h1>
<NavLink to="/2">Go to Page2</NavLink>
<Routes location={location}>
<Route path="/1" element={<SubPage1 />} />
<Route path="/2" element={<SubPage2 />} />
<Route
path="*"
element={<Navigate to="/1" />} // redirects to "/1" instead of "/1/1"
/>
</Routes>
</MyContextProvider>
);
};
Add the missing wildcard matcher on the root routes to allow descendent route matching.
<Routes>
<Route path="/1/*" element={<Page1 />} />
<Route path="/2/*" element={<Page2 />} />
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
Redirect to the correct descendent route in Page1
. Use either relative target path "1"
(or "./1"
) or absolute target path "/1/1"
.
const Page1 = () => {
const location = useLocation();
return (
<MyContextProvider>
<h1>Page1</h1>
<NavLink to="/2">Go to Page2</NavLink>
<Routes location={location}>
<Route path="/1" element={<SubPage1 />} />
<Route path="/2" element={<SubPage2 />} />
<Route path="*" element={<Navigate to="1" />} />
</Routes>
</MyContextProvider>
);
};
The
MyContextProvider
is remounted even when descendent routes change.
This is caused by the root CSSTransition
component using a React key coupled to the current location's key. The key changes when the descendent route changes, and when React keys change then that entire sub-ReactTree is remounted.
function App() {
const location = useLocation();
return (
<SwitchTransition>
<CSSTransition
key={location.key} // <-- React key change remounts subtree
classNames="right-to-left"
timeout={200}
>
<Routes>
<Route path="/1/*" element={<Page1 />} />
<Route path="/2/*" element={<Page2 />} />
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
</CSSTransition>
</SwitchTransition>
);
}
A solution for this is to promote the MyContextProvider
higher in the ReactTree such that it remains mounted even while routes change. Wrap the SwitchTransition
component with the MyContextProvider
and remove MyContextProvider
from the Page1
component.
function App() {
const location = useLocation();
return (
<MyContextProvider>
<SwitchTransition>
<CSSTransition
key={location.key}
classNames="right-to-left"
timeout={200}
>
<Routes>
<Route path="/1/*" element={<Page1 />} />
<Route path="/2/*" element={<Page2 />} />
<Route path="*" element={<Navigate to="/1" />} />
</Routes>
</CSSTransition>
</SwitchTransition>
</MyContextProvider>
);
}
Page1
const Page1 = () => {
const location = useLocation();
return (
<>
<h1>Page1</h1>
<NavLink to="/2">Go to Page2</NavLink>
<Routes location={location}>
<Route path="/1" element={<SubPage1 />} />
<Route path="/2" element={<SubPage2 />} />
<Route path="*" element={<Navigate to="1" />} />
</Routes>
</>
);
};