Cache react-router route - javascript

I have the following code:
<View>
<Header />
<List />
</View>
If the user clicks on an edit button in a list item, react-router changes the page from /list to /list/{itemId}. This is a new page. After the user filled in some information and clicks on save, react-router changes back to /list. Now the full /list-page is rerendered!
Is there a way to cache a page, so react can detect the changes and rerender only them instead of the full page again?
It should also be possible to render the page directly (not over /list -> /list/{itemId}) so no modal solution I think.
I'm using react-router-redux v5.0.0-alpha.9 so the solution should be compatible with it. It should also be compatible with react-native and react-dom.

If you are working with react-router
Component can not be cached while going forward or back which lead to losing data and interaction while using Route
Component would be unmounted when Route was unmatched
After reading source code of Route we found that using children prop as a function could help to control rendering behavior.
Hiding instead of Removing would fix this issue.
I am already fixed it with my tools react-router-cache-route
Usage
Replace <Route> with <CacheRoute>
Replace <Switch> with <CacheSwitch>
If you want real <KeepAlive /> for React
I have my implementation react-activation
Online Demo
Usage
import KeepAlive, { AliveScope } from 'react-activation'
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Test />
</KeepAlive>
)}
</AliveScope>
)
}
The implementation principle is easy to say.
Because React will unload components that are in the intrinsic component hierarchy, we need to extract the components in <KeepAlive>, that is, their children props, and render them into a component that will not be unloaded.

Got into same problem recently and my initial nudge was to use something like react-router-cache-route or another package.
But main disadvantage of third-party packages is that they are usually slow to support new react-router versions and source code seemed to me overly complex for this problem.
So I researched further and stumbled on this suggestion from Dan Abramov and ended up with solution like this:
function App() {
const itemId = useParams<{ itemId: string }>();
return (
<Switch>
...
<Route
exact
path={['some-list-route', 'some-list-route/:itemId']}
>
<List style={itemId ? { display: 'none' } : undefined} />
{itemId && <ListItem />}
</Route>
...
</Switch>
)
}
We keep both list and item routes under single Route component to have control over their mounting behaviour. When we get item route — we hide our <List /> (but it stays mounted, "kept-alive") and conditionally mount <ListItem />.
We can also pass itemId to <List /> as a prop, if we need more control, like conditionally trigger data fetching only for list route.

Related

How to pass props to layout.js from page.js in Next.js 13?

I want to pass props to layout.js of Next.js 13. Here's what it'd look like:
// layout.js
export default Layout({children}) {
return (
<>
{/* I want to render different `text` depending on the page.js I'm rendering */}
<h1>{text}</h1>
{children}
</>
);
}
Unfortunately, I end up doing this:
// CustomLayout.js
export default Layout({children, text}) {
return (
<>
{/* I want to have different `text` depending on the Page.js I'm rendering */}
<h1>{text}</h1>
{children}
</>
);
}
My question is, is it possible to pass props to layouts in Next.js 13? Is there a better approach?
// In every page.js
import Layout from "somewhere/CustomLayout";
export default Page() {
return (
<Layout text="My text">
{/* My Page Content */}
</Layout>
);
}
No, you shouldn't pass in additional props to a layout.js file in nextjs 13 appdir.
As #yousoumar pointed out, you shouldn't be calling Layout yourself. It sits at the root (at least for all the components under it), so should be treated as such.
Your approach of creating a custom layout that passes text as a prop is a reasonable approach.
You shouldn't be trying to pass custom props to Layout, inside Next.js's new app directory, as this component is created by you, but Next.js itself would call it, by giving it two props:
children (required)
Layout components should accept and use a children prop. During rendering, children will be populated with the route segments the layout is wrapping. These will primarily be the component of a child Layout (if it exists) or Page, but could also be other special files like Loading or Error when applicable.
params (optional)
The dynamic route parameters object from the root segment down to that layout.
Wanting to pass custom props to it, I think, is because you wanna use it in different places, but the whole idea is to have one Layout by route segment. And subroutes inherit the layout of outer ones. For example, layout.js inside app/blog gets wrapped by layout.js inside app/.
Having Layout called inside page.js as you did, is like having any wrapper, but not a Layout as designed by Next.js with its specific abilities:
A layout is UI that is shared between multiple pages. On navigation, layouts preserve state, remain interactive, and do not re-render. Layouts can also be nested.
import Layout from "somewhere/CustomLayout";
export default Page() {
return (
// ⚠️ this would behave like a normal wrapper, but not as a Layout as Next.js defines it
<Layout text="My text">
{/* My Page Content */}
</Layout>
);
}

How do I configure a React router to load two components on top of each other (Overlay)?

I am trying to figure out how to configure my React router to load two overlaying components. I have a route /user/testUser/item/1. If item:id is in the url, an overlay component opens. However, I want the underlying component /user/testUser/ to load as well. When I close my overlay, that is, when I switch from /user/testUser/item/1 to /user/testUser/, the underlying user component is loaded initially. If the /user/testUser/item/1 overlay is open, the underlying user component is not loaded.
<Switch>
<PrivateRoute
exact
path="/user/:id*"
component={ProfileView}
/>
<PrivateRoute
exact
path="*/item/:id"
component={ShowItem}
/>
</Switch>
You can not render two routes at a time. You can manage it via optional params and render that component on a particular route.
Example :
<Switch>
<PrivateRoute
exact
path="/user/testUser/item/:id?"
component={ProfileView}
/>
</Switch>
const ProfileView = () => {
const { id } = useParams();
return (
<>
{id && <ShowItem />}
...
...
<OtherComponent />
</>
)
};
You can check conditionally id and based on that render the other components you want.
You can't really do it using routes, Switch selects only one Route's component to be rendered. You could probably experiment with having two Switch'es in parallel, but it sounds like the easiest way would be to handle displaying the overlay in the underlaying component.

React, Single Page Apps, and the Browser's back button

I know my question could simply have a "This cannot be done, this defines the purpose of SPA". But...
I navigate to mydomain.com in my REACT web app. This page loads data from the backend and populates elaborate grids. It takes it about 2 seconds to load and render.
Now I click a link on that elaborate page and navigate to mydomain.com/otherPage. When I click the browser's BACK button to return to mydomain.com, it's blank, and has to be rebuilt from scratch as SPA dictates the DOM must be erased and re-built with every page change (at least the page-specific dynamic parts of it, as routes can be inside a fixed layout of header/footer etc). I get that...
Other than migrating to nextJS and using SSR....
Is there any magic solution in REACT to somehow 'retain' the DOM for a page when navigating out of it, so that when you browser-back into it, that page is instantly shown and not rendered from scratch?
Yes, it is very much possible to switch routes while keeping the DOM rendered, but hidden!
If you are building a SPA, it would be a good idea to use client side routing. This makes your task easy:
For hiding, while keeping components in the DOM, use either of the following css:
.hidden { visibility: hidden } only hides the unused component/route, but still keeps its layout.
.no-display { display: none } hides the unused component/route, including its layout.
For routing, using react-router-dom, you can use the function children prop on a Route component:
children: func
Sometimes you need to render whether the path matches
the location or not. In these cases, you can use the function children
prop. It works exactly like render except that it gets called whether
there is a match or not.The children render prop receives all the same
route props as the component and render methods, except when a route
fails to match the URL, then match is null. This allows you to
dynamically adjust your UI based on whether or not the route matches.
Here in our case, I'm adding the hiding css classes if the route doesn't match:
App.tsx:
export default function App() {
return (
<div className="App">
<Router>
<HiddenRoutes hiddenClass="hidden" />
<HiddenRoutes hiddenClass="no-display" />
</Router>
</div>
);
}
const HiddenRoutes: FC<{ hiddenClass: string }> = ({ hiddenClass }) => {
return (
<div>
<nav>
<NavLink to="/1">to 1</NavLink>
<NavLink to="/2">to 2</NavLink>
<NavLink to="/3">to 3</NavLink>
</nav>
<ol>
<Route
path="/1"
children={({ match }) => (
<li className={!!match ? "" : hiddenClass}>item 1</li>
)}
/>
<Route
path="/2"
children={({ match }) => (
<li className={!!match ? "" : hiddenClass}>item 2</li>
)}
/>
<Route
path="/3"
children={({ match }) => (
<li className={!!match ? "" : hiddenClass}>item 3</li>
)}
/>
</ol>
</div>
);
};
styles.css:
.hidden {
visibility: hidden;
}
.no-display {
display: none;
}
Working CodeSandbox: https://codesandbox.io/s/hidden-routes-4mp6c?file=/src/App.tsx
Compare the different behaviours of visibility: hidden vs. display: none.
Note that in both cases, all of the components are still mounted to the DOM!
You can verify with the inspect tool in the browser's dev-tools.
Reusable solution
For a reusable solution, you can create a reusable HiddenRoute component.
In the following example, I use the hook useRouteMatch, similar to how the children Route prop works. Based on the match, I provide the hidden class to the new components children:
import "./styles.css";
import {
BrowserRouter as Router,
NavLink,
useRouteMatch,
RouteProps
} from "react-router-dom";
// Reusable components that keeps it's children in the DOM
const HiddenRoute = (props: RouteProps) => {
const match = useRouteMatch(props);
return <span className={match ? "" : "no-display"}>{props.children}</span>;
};
export default function App() {
return (
<div className="App">
<Router>
<nav>
<NavLink to="/1">to 1</NavLink>
<NavLink to="/2">to 2</NavLink>
<NavLink to="/3">to 3</NavLink>
</nav>
<ol>
<HiddenRoute path="/1">
<li>item 1</li>
</HiddenRoute>
<HiddenRoute path="/2">
<li>item 2</li>
</HiddenRoute>
<HiddenRoute path="/3">
<li>item 3</li>
</HiddenRoute>
</ol>
</Router>
</div>
);
}
Working CodeSandbox for the reusable solution: https://codesandbox.io/s/hidden-routes-2-3v22n?file=/src/App.tsx
For API calls
You can simply put your generated elements that need intensive calculation in a state, in a component that never gets unmounted while changing page.
Here is an example with a Parent component holding 2 children and some JSX displayed after 5 seconds. When you click on the links you navigate to children, and when you click on browser's back button, you get back on the URL path. And when on / path again, the "intensive" calculation needing element is displayed immediately.
import React, { useEffect, useState } from "react";
import { Route, Link, BrowserRouter as Router } from "react-router-dom";
function Parent() {
const [intensiveElement, setIntensiveElement] = useState("");
useEffect(() => {
const intensiveCalculation = async () => {
await new Promise((resolve) => setTimeout(resolve, 5000));
return <p>Intensive paragraph</p>;
};
intensiveCalculation().then((element) => setIntensiveElement(element));
}, []);
return (
<Router>
<Link to="/child1">Go to child 1</Link>
<Link to="/child2">Go to child 2</Link>
<Route path="/" exact>
{intensiveElement}
</Route>
<Route path="/child1" exact>
<Child1 />
</Route>
<Route path="/child2" exact>
<Child2 />
</Route>
</Router>
);
}
function Child1() {
return <p>Child 1</p>;
}
function Child2() {
return <p>Child 2</p>;
}
About redisplaying quickly the DOM
My solution above works for not doing slow things twice like API calls. But following the remarks of Mordechai, I have made an example repository to compare DOM loading time of really big HTML for 4 solutions when using browser back button:
Plain html without javascript (for reference)
React with the code example I gave above
Next.js with next's page routing
A CSS solution with React and overflow: hidden; height: 0px; (more efficient than display: none; and the elements do not take any space contrary to visibility: hidden;, opacity: 0; etc. but maybe there is a better CSS way)
Each exemple loads an initial page of 100 000 <span> elements, and has links to navigate to small pages, so that we can try the back button on the browser.
You can test yourself the static version of the examples on github pages here (the pages take several seconds to load on a normal computer, so maybe avoid clicking on them if on mobile or so).
I've added some CSS to make the elements small enough to see all of them on the screen, and compare how does the browser update the whole display.
And here are my results:
On Firefox:
Plain HTML loads in ~2 sec, and back button displays page in ~1 sec
Next app loads in ~2 sec, and back button displays page in ~1 sec
CSS solution in React app loads in ~2 sec, and back button displays page in ~1 sec
React app loads in ~2.5 sec, and back button displays page in ~2 sec
On Chrome:
CSS solution in React app loads in ~2 sec, and back button displays page in ~1 sec
React app loads in ~2.5 sec, and back button displays page in ~2 sec
Plain HTML loads in ~8 sec, and back button displays page in ~8 sec
Next app loads in ~8 sec, and back button displays page in ~8 sec
Something important to note also: for Chrome when Next.js or plain HTML take 8 seconds, they actually load elements little by little on the page, and I have no cache with the back button.
On Firefox I don't have that little by little displaying, either there is nothing or everything is displayed (like what I have on Chrome with react state usage).
I don't really know what I can conclude with that, except maybe that testing things is useful, there are sometimes surprises...
I've misread the question initially. I'll leave the initial answer for the case when a user goes to a page on another domain.
Updated answer
You've wrote in the comments
I was clear enough
Well... judging by discussions here, that's not the case.
Here some points to consider, and when you'll answer them that should be the solution to your problem... whatever it is:
Do you really need to make network calls on the components' mount? For an SPA it's usually a good idea to decouple your state and visual representations of it (plural!).
Obviously, you need come caching mechanism. But should it be "cache" of rendered nodes of some sort (as have been suggested in every other answer) or cache of data, received from the net, or both, is up to you. And SSR - is not a caching mechanism. It exists for other reasons.
Do you use any router? If, yes, then which one and how? Because some of then can retain the previous route in memory, so with a little bit of luck you could've never stumble on you blank page problem. And that can be the answer.
But maybe mydomain.com/otherPage is not under control of the React or/and maybe it's not a true SPA we a talking about here. And the effects of going to this page is the same as going to another domain? Then my initial answer holds.
In a nutshell:
Is there any magic solution in REACT to somehow 'retain' the DOM for a page when navigating out of it.
Yes, if by navigating out of it and a page you mean navigating to another route in you SPA and just rendering some other component, without executing a GET request through a "standard" <a>-click, window.location.href change or something similar which will lead to the browser initiating a new page loading.
For that just read your router's docs.
No if your are actually leaving your SPA.
For this case I would suggest serviceWorker. As to my taste, it's a much simpler and more flexible solution compared to a change of the architecture of your project with SSR.
as SPA dictates the DOM must be erased and re-built with every page change
Not at all. DOM will be erased only if the state of a component or the props are changed. But to help you with that we need to see the code.
Initial answer
It's not totally clear what is your question about. You are focused on the idea of preventing the DOM rebuild, but at the same time you're saying that the bottleneck is the API calls. And they're two quite different things to deal with.
And possible solution to you problem heavily depends on the architecture of you code.
If you have control over the server side, you can setup caching for your calls. If not, you can setup caching on the client side in a PWA-style manner.
If you have a centralized store, you can save its state to the localStorage on an <a> click and repopulate your page from the localStorage when the user gets back onto your page. If not you can resort to Service Worker API again to intercept API calls and to return cached responses. (Or just override fetch or whatever)
You can even "emulate" the SSR by saving HTML to the localStorage and showing it right away when the user gets back. (But the page will not be fully functional for a couple of seconds and need to be replaced at the moment you API-calls are completed)
But there is no feasible way to prevent DOM rebuild, because while theoretically possible, it's probably impractical to cache the whole React internal state. And if your main problem is indeed the DOM rebuild itself then probably your code is in need of serious optimizations.
One solution I often use, is to persist that data in location.state. And then when navigating back, the component first checks for data in location.state before attempting to fetch the data again.
This allows the page to render instantly.
const Example = (props) => {
const history = useHistory();
const location = useLocation();
const initialState = location.state;
const [state, setState] = useState(initialState);
useEffect(() => {
const persistentState = state;
history.replace({ pathname: location.pathname }, persistentState);
},[state]);
return ();
}
Using out of the box routing, I would say: it's impossible.
But who said we need to use routes?
Solution 1:
Why not using Portals?
This probably won't work if you want to 'retain' the DOM for any navigation on your page. But if you want to 'retain' it on only one specific page, then you could just open a fullscreen portal/modal/dialog (or whatever you wanna call it).
Solution 2:
If you want to 'retain' the DOM for all navigation, then you could also write a "router-component" yourself.
The logic of your component could look like this:
First you need a lookup-table. Give every url a related component that should be rendered, when the url is called.
Check if the target url has previously been open
If no: create a new div and open the matching component (from the lookup) in it. Bring that div to the front (z-index)
If yes: bring the related (already existing) div to the front (z-index)
Writing such a component shouldn't be too hard. I only see two problems with it:
performance: if you got many overlapping components open at the same time, this could slow down your page (depending on how many pages and content you got)
on refresh everything gets lost

Updating other React components

I have a Navbar that has buttons that change the language of the website. Instead of using different htmls, I'm just replacing the text with what I have in a database. However, doing anything in a component only updates that component and I need a way to update every other one
I haven't tried anything useful yet, since there's a complicated layout for usual solutions.
App.js
function App() {
return (
<div className="App">
<Router>
<div className="container" id="main">
<Navbar />
<Switch>
<Route exact path="/" component={Home}></Route>
<Route path="/gallery" component={Gallery}></Route>
</Switch>
</div>
</Router>
</div>
);
}
export default App;
My Navbar is outside of Switch that contains all the different pages I need to update.
All I need to do is to somehow pass an update request to every other component. Would be nice if it'd happen without reloading the page, though that would somewhat work.
You don't mention using Redux, so I assuming you are just using React state. Keep the language in the state of the top-level component, and pass the language to the components that need it. Give Switch a call-back function that changes the top-level state when the language is changed.

Can I avoid re-rendering parent route handler component when the route changes?

I'm noobies in react / redux and I'm looking to create modal view following this 3 rules :
who is url dependant
who is generic
who is optimized
Cause of the url dependancy it's not optimized, it's rerender the parent.
Example: I have a view namespaces view at '/namespaces' who print all namespaces and when I open '/namespaces/edit' who open the modal view the namespaces is rerender. How to not rerender the namespaces list ?
Below the Router
<Route path="namespaces" component={NamespaceList}>
<Route path="edit" component={NamespaceEdit}/>
<Route path="create" component={NamespaceCreate}/>
</Route>
NamespacesList component
function NamespacesList({ push, children }) {
console.log("rendered !")
return (
<div>
NamespacesList
<p>
<Link to="/namespaces/create">Create</Link>
<br />
<Link to="/namespaces/edit">Edit</Link>
</p>
{children}
</div>
)
}
NamespacesCreate component (printed inside of the modal)
const NamespacesCreate = function() {
return (
<Modal>
NamespacesCreate
<p>
<Link to="/namespaces">Back to namespaces list ?</Link>
</p>
</Modal>
)
}
Use case :
I'm on the /templates/create (namespacelist is drawing in the back of the modal, in the console rendered ! is printed then when I click to link to comeback to the parent url /templates, rendered ! is printed again.
So there is a way to "optimize" it and not rerender the namespaceList or I need to choose between, or am I wrong ?
Don’t worry about how many times the component’s render() method is called. Rendering is very cheap in React, and if the content has not changed, it will not actually touch the DOM.
You should only start worrying about render() method calls when you start to experience real performance problems in your app which is very unlikely for one-off things like navigations. For example, you might want to optimize animations or form components that have ton of inputs.
If and when you have this problem (not earlier!), you can check out React guides to Advanced Performance optimizations, measuring wasted renders with ReactPerf, and learn about common performance anti-patterns.
Don’t let this complicate your code for no reason though. Only optimize when you have a real problem, and no sooner. React is very fast for most users’ needs out of the box, and render() method being called often is perfectly fine.

Categories

Resources