Fixed side menu on an specific Next.js route - reactjs

I'm starting a new project and I would like to always have a top nav and only in a specific route have a sidebar where I can click and the content will change.
The top nav should always be visible if I am in site.com or site.com/*/**
If I go to site.com/posts I want to see a sidebar with all the posts title
If I click a post on the left it will redirect to site.com/posts/1 and only the right side should change
I'm having trouble with the second and third bullet. My pages path is pages/posts/index.js and pages/posts/[id].js but how can I declare only one file and avoid duplicating code? 🤔
I tried pages/posts/[[...slug]].js but I'm seeing this error: Error: Optional catch-all routes are currently experimental and cannot be used by default ("/posts/[[...slug]]")
I'm looking for examples but so far I couldn't do it.
Any ideas?

Update: Next.js now recommends to use function getLayout()
New Documentation ✨
Create a file: /layouts/RequiredLayout.js
const RequiredLayout = ({ children }) => {
return (
<>
<Navbar />
<main>{children}</main>
</>
);
};
export default RequiredLayout;
Then edit _app.js
function MyApp({ Component, pageProps }) {
const Layout = Component.Layout || EmptyLayout;
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
const EmptyLayout = ({ children }) => <>{children}</>;
export default MyApp;
Now whenever you require to use this layout, then add only a single line at the end
NameOfComponent.Layout = RequiredLayout;
For in your case: In **pages/posts/[id].js**
import React from 'react'
const ID = () => {
return(
<>
<p>page with id details</p>
</>
)
}
export deafult ID;
ID.Layout = RequiredLayout //responsible for layout

I was able to enable this experimental option by having my next.config.js look like this (I'm using Next v9.4.4):
module.exports = {
experimental: {
optionalCatchAll: true,
},
};

Related

Definig page component only by HOC in NextJS

I have nextjs (react) website I use (on every "page") same template - there is Detail and List component.
I have definition of every page as XML (not nextjs thing but my implementation). Root element here is Forms and childrens are individual form defined like this:
<Form FID="Event">
<DetailFrame createNewEntryText="Add event">
<Component attributeKey="img_url" componentName="chose file" componentType="FileChooser" path="img/albums/other/event-photos/#[img_url]"/>
<Component attributeKey="title" componentName="Title" componentType="TextField" transformation="#[title]"/>
<Component attributeKey="date" componentName="Date" componentType="DateField" transformation="#[date]"/>
<Component attributeKey="description" componentName="Description" componentType="RichTextField" transformation="#[description]"/>
</DetailFrame>
<ListFrame orderBy="date" descending="true">
<Component attributeKey="id_event" componentName="ID" componentType="TextField" transformation="#[id_event]"/>
<Component attributeKey="img_url" componentName="Image preview" componentType="ImagePreview" transformation="img/albums/other/event-photos/#[img_url]" />
<Component attributeKey="title" componentName="Title" componentType="TextField" transformation="#[title]"/>
<Component attributeKey="date" componentName="Date" componentType="DateField" transformation="#[date]"/>
<Component attributeKey="description" componentName="Description" componentType="RichTextField" transformation="#[description]"/>
</ListFrame>
</Form>
Only thing what I need specify on every nextjs page is FID and individual component will then compose by their own by XML definition of form. All the stuff like menu and other components (except FormFrameContainer which is wrapper for Detail and List components) are defined in _document.tsx (= html wrapper for nextjs pages offered by nextjs) and _app.tsx (= components wrapper for nextjs pages offered by nextjs; wrapped by _document.tsx). To be clear - FormFrameContainer is wrapped by that stuffs in _app.tsx and _document.tsx.
So every administration page look almost same, like this:
const EventsPage: NextPage = (props: any) =>{
const dispatch = useAppDispatch();
useEffect(() => {
dispatch({ type: SagaActions.SET_FORM_DEFINITIONS_AND_SET_FORM_BY_ID, FID: "Event" });
}, [dispatch])
return (
<div>
<FormFrameContainer />
</div>
)
}
Because of repetition I decided to move useeffect logic into hoc named withAdminPage:
export default (Page: NextPage, formID: string) => {
return (props) => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch({ type: SagaActions.SET_FORM_DEFINITIONS_AND_SET_FORM_BY_ID, FID: formID });
}, [dispatch])
return <Page {...props} />;
}
}
So literaly the only thing what remains in particular pages is returning:
<div>
<FormFrameContainer />
</div>
What comes to my mind is also move it to hoc, because it is also same in all pages => duplication. But it would mean, that I will not actually define components (my hoc will generate them) and in every page file will be only this one line:
export default withAdminPage(EventsPage, "Event");
For me it seems logic but also as some anitpattern to hoc (because hoc is defined as function, that take component and return new (enhanced) component). So? What would be best practise here? Would it be violation of some code standards?

Problem with wrapping whole React App (all pages) in certain functionality

I have React App consisting of few pages. LandingPage (to be clear, it is really the first page in app flow)is like this:
export const LocalLandingPage = () => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return (...here goes actual content...);
};
const LandingPage = withRouter(wrappedInLinkToSearchHOC(WithSnackBarHOC(LocalLandingPage)));
Here I call hook useFavorites which gathers some methods dealing with a specific subset of local Storage content(and dispatches actions to Redux store as well). The code above return call is checks support for local Storage and tells if some specific items are stored and in what quantity.
The story is that I have realized that if someone enters not the LandingPage but any other page of the app, the code will not be executed and support for localStorage not checked.
Besides, it is really not LandingPage business to deal with storage, so it should be removed anywhere.
My idea was to write HOC and wrap the application. So, here is this HOC (to keep things simple it is initially JS not TS) To make useFavorites work, it had to be a hook, too.
import useFavorites from '../hooks/useFavorites';
const useCheckSupportForLocalStorage = Component => {
const { Favorites } = useFavorites();
// React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
// }, []);
return props => <Component {...props} />;
};
export default useCheckSupportForLocalStorage;
Having it done, I have tried to use it on App like this
function App() {
return (
<Switch>
... here are routes...
</Switch>
);
}
export default useCheckSupportForLocalStorage(App);
It throws an error:
React Hook "useCheckSupportForLocalStorage" cannot be called at the top level.
That is truth, no doubt. So, the next idea was to create a temporary component from all routes.
function Routes() {
const Routes = useCheckSupportForLocalStorage(
<>
<Route exact path={Paths.landing} component={Awaiting(StarWars)} />
...here is the rest or outes...
</>,
);
return <Routes />;
}
export default Routes;
And use it in rewritten App like this
function App() {
return (
<Switch>
<Routes />
</Switch>
);
}
but it throws error
Routes' cannot be used as a JSX component.
Its return type '(props: any) => Element' is not a valid JSX element.
Type '(props: any) => Element' is missing the following properties from type 'ReactElement<any, any>': type, props, key
Forcing useCheckSupportForLocalStorage th have return type of ReactElement doesn't help, just leads to other error. I have checked few others options as well. What is wrong, how should it be written?
Basically I could stop using useFavorites in this hook, then it would be just a function - but it would be extreme headache.
You say
To make useFavorites work, it had to be a hook, too.
That is not true, since you have it in a component it will work just fine. And my understanding is that you want to wrap your whole app with it, so no need for the HOC. Just use it at the top of your app hierarchy.
something like
import useFavorites from '../hooks/useFavorites';
const CheckSupportForLocalStorage = ({ children }) => {
const { Favorites } = useFavorites();
React.useEffect(() => {
Favorites.manageSupport() && Favorites.showSize();
}, []);
return children;
};
export default CheckSupportForLocalStorage;
and
function App() {
return (
<CheckSupportForLocalStorage>
<Switch>
... here are routes...
</Switch>
</CheckSupportForLocalStorage>
);
}
export default App;

Use swr with Next global context: entire page gets re-rendered

I have the following piece of code:
function MyApp({ Component, pageProps }: AppProps) {
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
return (
<React.StrictMode>
<CSSReset />
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<ChakraProvider theme={theme}>
<AuthenticationContext.Provider value={{
tronLinkAuth, tronLinkLoading, mutateTronLink,
authenticatedUser, authLoading, authLoggedOut, mutateAuth
}}>
<Component {...pageProps} />
</AuthenticationContext.Provider>
</ChakraProvider>
</React.StrictMode>
)
}
Example useAuthenticatedUser:
export default function useAuthenticatedUser() {
const { data, mutate, error } = useSWR("api_user", fetcher, {
errorRetryCount: 0
});
const loading = !data && !error;
const loggedOut = error && error instanceof UnauthorizedException;
return {
authLoading: loading,
authLoggedOut: loggedOut,
authenticatedUser: data as AuthenticatedUser,
mutateAuth: mutate
};
}
The code works, but my entire webpage gets re-rendered when swr propagates its result.
For example:
const Login: NextPage = () => {
console.log('login update');
return (
<>
<Head>
<title>Register / Login</title>
</Head>
<Navbar />
<Box h='100vh'>
<Hero />
</Box>
<Box h='100vh' pt='50px'>
Test second page
</Box>
</>
)
}
export default Login;
When using useContext in the Navbar, it also re-renders the entire LoginPage, including the Hero, while this is not my purpose.
const Navbar: React.FC = () => {
const authState = useContext(AuthenticationContext);
...
I'm also confused as for why the logs appear in the server console, as this is supposed to be executed client-side.
Edit: not an issue, this is only on first render.
How to solve?
I'm interested in using swr for this use case, because it allows me to re-verify the authentication status e.g. on focus but use the cached data meanwhile.
Edit:
Confusing. The following log:
function MyApp({ Component, pageProps }: AppProps) {
console.log('app');
const { tronLinkAuth, tronLinkLoading, mutateTronLink } = useTronLink();
const { authenticatedUser, authLoading, authLoggedOut, mutateAuth } = useAuthenticatedUser();
Also gets printed out every time I switch tabs and activate the swr.
So it re-renders the entire tree? Doesn't seem desirable...
I currently went with the easier solution, i.e. use useSwr immediately on the component that uses the data.
From the docs:
Each component has a useSWR hook inside. Since they have the
same SWR key and are rendered at the almost same time, only 1 network
request will be made.
You can reuse your data hooks (like useUser in the example above)
everywhere, without worrying about performance or duplicated requests.
So it can be leveraged to re-use it wherever needed without having to worry about global state re-renders.
In case there is an alternative response how to use the global Context Provider, don't hesitate to share.

Using Persistent Layouts in NextJS with Higher Order Functions

Im using Adam Wathan's method for using persistent layouts in Next. Is there a way to get them to work with Higher Order Functions? I'm not really sure how HOFs work.
My _app.js
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || (children => <>{children}</>)
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
A sample page looks like this
const Home = () => {
return (
<>
...
</>
)
}
Home.Layout = BaseLayout;
export const getServerSideProps = withAuthUserTokenSSR()()
export default withAuthUser()(Home)
If I remove the HOF the layouts work fine, otherwise I get:
Error: Objects are not valid as a React child (found: object with keys {children}). If you meant to render a collection of children, use an array instead.
You need to apply the layout component to the higher-order component itself, as it's probably wrapping your original Home component and hiding Home.layout away.
const Home = () => {
return (
<></>
)
}
const HomeWithAuth = withAuthUser()(Home)
HomeWithAuth.layout = BaseLayout;
export default HomeWithAuth
Also, make sure you use the same variable name (same casing, e.g., layout vs. Layout) in your page component and when you refer to it in _app.

Gatsby application that is SPA on mobile and has different routes on desktop

📖 Summary
Recently my team started a project of a landing page and we chose to use Gatsby in order to have good SEO.
At a point in our project, the designers changed the mobile layouts to be a SPA, and the desktop ones still having different routes and pages.
Refer to that example:
Since Gatsby creates pages in build time, we don't know if the environment is mobile or desktop, it's difficult to think in a way to deal with that behavior.
🐛 Workaround
One quick way that our team thought to temporarily resolve that problem was to map between sections and hide than in desktop screens.
And the biggest problem is: On the first load of the page the content takes almost a second to load because it's not static anymore.
<div>
{
breakpoints.md
? pages.map((page) => renderPage(page))
: renderPage(selectedPageRef.current)
}
</div>
🚀 Goals
I would like to discuss about a solution that will change the behavior of the pages in desktop and mobile without killing the SEO of the application.
If you can't solve by using mediaqueries and you must display two different components rather than the same styled. The workaround to solve this is to check what is the window size at the rendered time and show one layout or another. It would create a minimum delay (insignificant if cached) before the header is shown but this is the only way I am able to guess.
So, using Gatsby's default structure in <Layout> component you should have something like that:
return (
<>
<Header siteTitle={data.site.siteMetadata.title} />
<div>
<main>{children}</main>
<footer>
© {new Date().getFullYear()}, Built with
{` `}
Gatsby
</footer>
</div>
</>
)
So, in your <Header> component you should check your window size and render one component or another:
export const Header = (props) => {
let currentWidth;
if (typeof window !== 'undefined') currentWidth = useWindowWidth();
return typeof window !== 'undefined' ? currentWidth >= 768 ? <DesktopPane /> : <MobilePane /> : null;
};
As you can see, in the return I check if window is defined in a ternary chained condition. If the window is not defined (i.e: is undefined) it returns a null. If it's defined, it checks the current window's width (currentWidth) and there's another ternary condition that displays the mobile or the desktop menu.
As a best practice, chained ternaries are not the cleanest solution, they are difficult to read and maintain but for now, the solution works (and of course it must be refactored).
In this case, useWindowWidth() is a custom hook that calculates in every window the size but you can use whatever you like. It looks like:
import {useEffect, useState} from 'react';
const getWidth = () => window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
export const useWindowWidth = () => {
let [width, setWidth] = useState(getWidth());
useEffect(() => {
let timeoutId = null;
const resizeListener = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => setWidth(getWidth()), 150);
};
window.addEventListener('resize', resizeListener);
return () => {
window.removeEventListener('resize', resizeListener);
};
}, []);
return width;
};
Code provided by: https://usehooks.com/useWindowSize/
Note that it's common in Gatsby's projects to check if the window is !== than undefined due to the point you argued. At the compilation/build point isn't defined yet. You can check for further information about Debugging HTML Builds in their documentation.
First, you can achieve the following with the help of CSS media query (hide the sidebar or transform it to be the top nav):
Then, you can set up a useMediaQuery hook (for example here is an implementation) to conditionally render the About and Events components in one of the two ways shown below:
A) If most users are using desktop, you can defer the import of the other two components (About and Events) in mobile view with loadable-components:
The Index page will look something like this:
import React from "react"
import Loadable from "#loadable/component"
import useMediaQuery from "use-media-query-hook"
import Sidebar from "../components/Sidebar"
import Home from "../components/Home"
const LoadableAbout = Loadable(() => import("../components/About"))
const LoadableEvents = Loadable(() => import("../components/Events"))-
const IndexPage = () => {
const isMobile = useMediaQuery("(max-width: 425px)")
return (
<div>
<Sidebar />
<Home />
{isMobile && <LoadableAbout />}
{isMobile && <LoadableEvents />}
</div>
)
}
export default IndexPage
B) If most users are using mobile, you can include the two components in the main bundle at build time:
The Index page will look something like this:
import React from "react"
import useMediaQuery from "use-media-query-hook"
import Sidebar from "../components/Sidebar"
import Home from "../components/Home"
import About from "../components/About"
import Events from "../components/Events"
const IndexPage = () => {
const isMobile = useMediaQuery("(max-width: 425px)")
return (
<div>
<Sidebar />
<Home />
{isMobile && <About />}
{isMobile && <Events />}
</div>
)
}
export default IndexPage
Regarding SEO, the search engine will only see the main component (Home in route "/", About in "/about", etc) since isMobile defaults to null at build time in the above implementations.
Regarding speed, the main components are statically rendered in the HTML. Only in mobile SPA view, other sections are needed to be loaded.

Resources