InertiaJS How to use "Nested Layouts" for tabs? - javascript

I was hoping to get some additional information about the "Nested Layouts" in InertiaJS. The documentation is very scarce on it and I can barely find any sort of examples showing how it works, and the documentation on the site isn't very descriptive about how it works or what the code is doing. (https://inertiajs.com/pages#persistent-layouts)
Basically I want to achieve functionality similar to in this tweet here;
https://twitter.com/klaasgeldof/status/1181198792995037184
Hopefully someone can provide some extra information because I've been having a lot of trouble getting this working correctly.

There are many approaches to this and I'd say they are more related to what your frontend stack is. Since this question is tagged with vue.js I'll answer based on that.
Approach #1
The inertia part here is just creating a layout - which is basically just a vue component. That layout then contains the side and top navigation and a slot to fill the body content. You can then create a dashboard page component which utilize that layout by adding layout: myDashboardLayout in the export.
To get the same effect, you basically route to different views, which pass different slots to the layout and their respective data.
// Your DashboardLayout.vue
<template>
<div>
<my-sidebar :data="foo" />
<my-topbar :data="bar" />
<slot name="dashboardContent" />
</div>
</template>
// Your Dashboard/Index.vue
<template>
<main slot="dashboardContent" :data="myData" />
</template>
<script>
import DashboardLayout from './DashboardLayout'
export default {
// Using the shorthand
layout: DashboardLayout,
}
</script>
//web.php
Route::get('/dashboard', function(){
return Inertia::render('Dashboard/Index', [data]);
});
Route::get('/dashboard/foo', function(){
return Inertia::render('Dashboard/Index', [fooData]);
});
Route::get('/dashboard/bar', function(){
return Inertia::render('Dashboard/Index', [barData]);
});
You then either visit domain.com/dashboard/foo or domain.com/dashboard/bar
Approach #2
Having the same layout view, but passing a "tab-view" component. You'd not switch the routes at all and provide that one tab-view component with props to render the three different tabs.
Depending on the data, this approach could be slower as you fetch everything up front.
With #1 you fetch the data on demand and since it's an SPA, the user would't notice a difference.
Another benefit of #1 is, the user can actually actively visit that one specific tab and bookmark its URL for frequent access. You could do the same with anchors/hashes but you're using an SPA after all.
I could go on, but choose either option or perhaps that was enough to give you some inspiration.

Add a <slot /> inside tab
Now the trick is to put two Layouts on the nested page.
https://inertiajs.com/pages
<script>
import SiteLayout from './SiteLayout'
import NestedLayout from './NestedLayout'
export default {
// Using a render function
layout: (h, page) => {
return h(SiteLayout, [
h(NestedLayout, [page]),
])
},
// Using the shorthand
layout: [SiteLayout, NestedLayout],
props: {
user: Object,
},
}
</script>
layout: [SiteLayout, NestedLayout] this is where the trick is.

Related

Is it possible to load stylesheets for React component by extension when env is specified?

I've read a bit about <ThemeProvider />, React.lazy, React Helmet, and a few others but they don't seem to do what I would like; unless they do and I'm looking at their uses incorrectly or am using the wrong terms to search and research - if so, please let me know as it would be very helpful to have someone go "These are what you want to use, you just need to continue working with them to get it".
I have a website with shared components that have two stylesheets each containing color and sizing variables, one for Light and one for Dark. Ideally, I want the shared components to determine when to load the respective Light and Dark styles without needing to place conditional within each and every component, ie:
import lightStyle from 'div.module.scss';
import darkStyle from 'div.dark.module.scss';
...
<div className={ApplicationVariableService.isDarkMode() ? darkStyle.div : lightStyle.div}>
<p className={ApplicationVariableService.isDarkMode() ? darkStyle.p: lightStyle.p}>
{children}
</p>
</div>
...
rather I want to apply className={styles} to all the elements.
I got close with the following:
import React from 'react';
let styles;
if (ApplicationsVariableService.isDarkMode()){
import("./div.dark.module.scss")
.then((res) => {
styles = res;
});
} else {
import("./div.module.scss")
.then((res) => {
styles = res;
});
}
...
return(
<div className={styles}>{children}</div>
)
The downside is that this block would need to be added to each and every component not only replicating the same thing over and over but should additional stylesheets come in later all of these blocks will need to be modified.
I'm thinking I could import then export all of the component stylesheets into a light_index.js and dark_index.js then add the if/else to the main index.js file that exports all of the components for use in the App. That could potentially hit load speed because I don't want to load all of the component stylesheets, just the stylesheets for the component rendering on the page.
My current file structure:
// shared components
src
stylesheets
_light_variables.scss
_dark_variables.scss
components
div
div.jsx
div.module.scss // imports light_variables
div.dark.module.scss // imports dark_variables
index.js
// App
app
src
components
page.jsx
index.js
Maybe I'm over thinking it? Can I have this file structure and the desired behavior without loading both light and dark stylesheets for every component? I am thinking about the future where more stylesheet might be added later and I wouldn't want to have to edit every components conditional import statement or the condition rendering in each and every element. Was I looking in the right direction with <ThemeProvider /> and React.lazy?
Guidance would be much appreciated!
Update: I completely redid my approach as explained in my other question. For edification and anyone else wanting to do something similar, if you have a solution please share it!

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

Is it possible to mix two GraphQL queries into one query?

I am using Gatsby as a frontend to a WordPress backend. I have successfully fetched the menu items but I now need to link them to the pages I have created. I have added the Menu Links in my Gatsbyconfig.js but I have no idea on how I can go about mapping it to the menu items at the same time as the menu itself. It ends up contradicting each other. Is this possible to do? I am quite new at graphQL. Never touched it till this project. Below is the GraphQl Query
{
allWpMenuItem(filter: {menu: {node: {name: {eq: "Navigation Menu"}}}}) {
edges {
node {
label
}
}
}
site {
siteMetadata {
title
menuLinks {
name
link
}
}
}
}
The label holds the name for each menu and the link holds the link to the pages I have created. I am trying to fix this into this bit of code
<div>
{props.allWpMenuItem.edges.map(edge =>(
<a href="#" key={edge.node.label}>
{edge.node.label}
</a>
))}
</div>
I am trying to query the menu links change the anchor tag to the link item and then point it to the menu link.
The short answer is that you can't, natively each query is a separate entity. However, you can combine queries using createResolvers, for further details check: https://github.com/gatsbyjs/gatsby/discussions/28275 (thanks LekoArts for the feedback).
However, you have a few approaches to bypass this limitation. Given that each query is transformed into a standalone JavaScript object, you can always manipulate them using native JavaScript filtering or doing whatever you need. The idea is to create an empty object and populate it with links and labels, in a single loop or using two different, then, in your component, iterate through this object instead of through the props like you are doing now.
I can't create a minimal example without knowing the internal structure or without knowing what's inside menuLinks.name.
By the way, if you are using internal navigation I would hardly suggest using <Link> component, among other things, using the #reach/router (extended from React's router) will only render the necessary parts of each component, creating smoother navigation than using native anchor tag, what will refresh all the page.

Display multiple router components in a single view on Desktop

I'm using vue.js to build a PWA. Despite the fact that this isn't a desktop browser app, I'm thinking of ways to better utilise the otherwise wasted real-estate by combining multiple views into a single larger view.
For example, on mobile, there are 2 views with distinct routes - Select Outlet and Order Items. Is there any way to combine both these views only for the desktop with minimal modification? Thank you!
Abstract
I have a proposition of solution. Not sure if it will satisfy you but maybe it will give you a new perspective at least.
Example
Router setup:
const routes = [
{
path: '/combined-components',
name: 'CombinedComponents',
components: {
default: WrapperComponent,
'firs-component': SelectOutlet,
'second-component': OrderItems
},
}]
Template setup:
<div class="wrapper-component">
<div class="container">
<div class="first-component">
<router-view name="first-component"></router-view>
</div>
<div v-if="isMobile" class="second-component">
<router-view name="second-component"></router-view>
</div>
</div>
<div>
Explanation
By using the name attribute for <router-view> we can decide which component will be displayed depending on the current route. So we have one parent component in this case WrapperComponent and inside it's template we can freely place another <router-view> and in routes.js (or just routes const that you pass to configure router) you can specify which component you want to be placed in the slot of first-component and second-component. That allows you to combine 2 of them and just proceed further routes without leaving WrapperComponent. Wha is left is to define if you should display the second component or not. Depending on isMobile that you can define yourself (I would assume based on screen width).
Docs
Read more here: Vue Router Docs
Summary
In this approach, it works from the other side. You prepare it for desktop and just restrict it for mobile.

Pass reference of a component to another one in ReactJS

Before anyone press eagerly the close button, I already have looked the following question: ReactJS Two components communicating. My problem is exactly the third scenario developped in the current accepted answer.
I am using ReactJS to build something with two components. For HTML reasons (and presentation), i want my two components to be at two different places of the page.
For the moment, I have the following pattern, corresponding to scenario #2:
FooForm = React.createClass({
...
});
FooList = React.createClass({
...
});
FooManager = React.createClass({
...
render: function () {
return (
<div>
<FooForm ref="form" manager={this} />
<FooList ref="list" />
</div>
);
}
});
React.render(
<FooManager someProp={value} />,
document.getElementById('foo')
);
This gives something like:
<div id="foo">
<form>Form generated with the render of FooForm</form>
<ul>List generated with the render of FooList</ul>
</div>
However, i would like to have something like this:
<div id="fooform">
<form>Form generated with the render of FooForm</form>
</div>
<!-- Some HTML + other controls. Whatever I want in fact -->
<div>...</div>
<div id="foolist">
<ul>List generated with the render of FooList</ul>
</div>
The problem here is: how can I keep a reference in each component? Or at least the link Form -> List?
I tried to create the FooList before and pass the reference to the current manager, but I get the following warning/error:
Error: Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. This usually means that you're trying to add a ref to a component that doesn't have an owner (that is, was not created inside of another component's `render` method). Try rendering this component inside of a new top-level component which will hold the ref.
The documentation says you can attach events to link two components which do not have a parent-child relation. But I don't see how. Can someone give me some pointers?
The Less Simple Communication lesson from react-training has a good example of how you can move actions & state sideways to avoid having to create an explicit link between related components.
You don't need to jump into a full Flux implementation to get the benefit of this approach, but it's a good example to lead you up to Flux, should you eventually need it or something like it.
Note that this requires you to model the relationship between the components based on changing state rather than explicitly passing a reference to a component instance (as you're doing above) or a callback bound to the component managing the state.
This would be the perfect use-case for a Flux type architecture.
What you want is someone FooManager to be able to trigger state changes in both components. Or, in fact, having the different components trigger, through Actions, state changes in each other.
The Flux Todo-App Tutorial illustrates your use-case perfectly!
After this, then you'd have the choices of using Facebooks implementation of Flux or the other gazillion ones.
My personal favorite is Reflux

Categories

Resources