Extensible layout components in nextjs - reactjs

I am creating an application in nextjs. I understand that I can generate a layout like the example given in the docs
import Navbar from './navbar'
import Footer from './footer'
export default function Layout({ children }) {
return (
<>
<Navbar />
<main>{children}</main>
<Footer />
</>
)
}
However, I would like to change the contents of the Navbar on a per page basis.
export default function ListPage() {
return {
/** I want to add a secondary nav to the Navbar on this page */
<>
<Navbar><MySecondaryNav></Navbar>
....
</>
}
}
export default function ClientPage() {
return {
/** I want to add a a different secondary nav to the Navbar on this page */
<>
<Navbar><ClientNavbar></Navbar>
....
</>
}
}
I also need the markup to be rendered server-side. Is it possible to achieve this in nextjs?

Yes, you can!
You could use useRouter and then depending on the path render the correct component in NavBar
import { useRouter } from "next/router";
const router = useRouter();
now you should have access to router.pathname, depending on the pathname just render the correct component inside NavBar
#Edit - SSR solution
If you want this SSR, then this is how you do it
in your _app.tsx/jsx you can use getServerSideProps and then get directly resolvedUrl
export default function App({ Component, pageProps }) {
return (
<Layout {...pageProps}>
<Component {...pageProps} />
</Layout>
);
}
export async function getServerSideProps({{req, res, resolvedUrl}) {
return { props: { resolvedUrl } }
}
At this point you should be receiving resolvedUrl and based on that render the required component.
If this wont work for you we might need to setup a codesandbox

Related

Passing state from parent to children in React Nextjs

I am a beginner in React and I am facing an issue to pass a state from parent to children.
That's my code :
import Footer from './Footer';
import Header from "./Header";
export default function Layout({setPage, children}) {
return <>
<Header setPage={setPage}/>
{children}
<Footer/>
</>
}
At this step, I get the setPage state and I can pass it successfully to the Header component for example. However, {children} don't get it. Is there a way to pass the setPage state to {children} ?
Thanks,
I tried useContext hook but didn't work...
Assuming you are using this Layout component to wrap certain pages content, I don't think you'll need to do this. Follow along:
Probably, you'll use this component as follows:
import { useState } from "React";
import Layout from "../components/Layout";
import OtherComponent from "../components/OtherComponent";
export default function HomePage() {
const [page, setPage] = useState();
return (
<Layout setPage={setPage}>
<OtherComponent setPage={setPage}/>
</Layout>
);
};
Because you'll need to know which children will use that prop. React does have some ways to do this that you asked, but it's discouraged as it fit a super small set of cases.
Keep up learning, good luck!
Edit 1: To complement the usage, you can change what is rendered on the screen using that state like this:
/* previous imports */
import YetOtherComponent from "../components/YetOtherComponent";
export default function HomePage() {
const [page, setPage] = useState(0); /* setting the default inside the parenthesis */
return (
<Layout setPage={setPage}>
{page === 1 /* checking which component should render */
? (<OtherComponent setPage={setPage}/>)
: page === 2
? (<YetOtherComponent setPage={setPage}/>)
: /* As many options as you want */
}
</Layout>
);
};
This way not all of them would display at all times, only when the respective page value is equal to the component that must be shown.
There are several ways to achieve the objective. Described below is one rather fundamental / rudimentary method.
Layout component:
import Footer from './Footer';
import Header from "./Header";
export default function Layout({setPage, children}) {
return <>
<Header setPage={setPage}/>
{children}
<Footer/>
</>
}
Parent / page where layout component is included:
import Layout from './Layout';
export default function SomePage(props) {
const [page, setPage] = useState();
return <>
<Layout setPage={setPage}>
<SomeChild1 setPage={setPage} />
<SomeChild2 setPage={setPage} />
<SomeOtherChild />
</Layout>
</>
}
In this way, the setPage method is passed to each child where it is required. SomeChild1 and SomeChild2 both have the setPage sent as prop. Whereas SomeOtherChild does not require the prop and therefore does not have it.

Pass page variables to layout in NextJS

I'm migrating my blog from Jekyll to NextJS and want to achieve the same functionality with the site layout.
The idea is to have metatags defined in a single layout file and fill values with variables that are defined on a page level.
I saw several solutions, one of them is to define metatags in _app.js as described here:
NextJS pass class from page to layout component
but from my understanding so far as a newbie in React/NextJS, it's better to utilize pageProps, but I can't figure out how to code it.
So far I have
_app.js
import 'bootstrap/dist/css/bootstrap.css'
import Layout from '../components/layout/layout';
export default function Blog({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
layout.js
import { Fragment } from 'react';
import Image from 'next/image';
import Head from 'next/head';
import MainNavigation from './main-navigation';
export default function Layout({ children }) {
return (
<Fragment>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<title>{children.title}</title>
</Head>
<MainNavigation />
<main>
{children}
</main>
</Fragment>
);
}
HomePage.js
export default function HomePage() {
return <div>HomePage</div>
}
I wanted to stick to the original code examples from the official documentation, so left layout as described in question, just
<Layout>
not
<Layout metas={pageProps.metas} ...>
So I just had to define props using getStaticProps:
export async function getStaticProps() {
return { props: { title: 'HomePage' } }
}
export default function HomePage() {
return <div>HomePage</div>
}
and then call it in layout as
{children.props.title}
not
{children.title}
The latter one would work if I just define a regular js variable in HomePAge.js as described in another SO thread I referenced. But I'm not sure if this approach somehow affects static website generation, so I decided to use NextJS built-in feature getStaticProps.
You can take the props you want in pageProps and pass them to the layout component:
<Layout metas={pageProps.metas} ...>
And use them in the <Layout />:
export default function Layout({ metas, children }) {
return (
<Fragment>
<Head>
<title>{metas.title}</title>
</Head>
...
</Fragment>
);
}

There is a way to remove component <Header /> inside _app.tsx in one single page?

I have a component inside my _app.tsx file, This componennt is rendered in all pages, but I want that in my page payment.tsx this component doesnt exist, there is some way to do it?
Something like this should work:
// pages/payment
export default function Payment() {
// ...
}
Payment.hideHeader = true;
// pages/_app
import Header from '...';
export default function App({ Component, pageProps }) {
return (
<>
{!Component.hideHeader && <Header />}
<Component {...pageProps} />
</>
);
}
You might need to customize types, etc. as you're using TS. Refer this: https://nextjs.org/docs/basic-features/layouts#with-typescript.

How to provide context from contextApi for a specific set of routes in nextjs and preserve state with routing through linking?

I am using contextApi with nextjs and I'm having some trouble when providing a context just for certain routes. I am able to make the context available for just a few routes, but when I transition from one to the other through linking, I end up losing the state of my application.
I have three files inside my pages folder:
index.tsx,
Dashboard/index.tsx and
SignIn/index.tsx.
If I import the provider inside the files Dashboard/index.tsx and SignIn/index.tsx and go from one page to the other by pressing a Link component from next/link, the whole state is set back to the initial state.
The content of the Dashboard/index.tsx file
import React from 'react';
import Dashboard from '../../app/views/Dashboard';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
export default Index;
This is the contend of the SignIn/index.tsx file:
import React from 'react';
import SignIn from '../../app/views/SignIn';
import { AuthProvider } from '../../contexts/auth';
const Index: React.FC = () => (
<AuthProvider>
<SignIn />
</AuthProvider>
);
export default Index;
The views folder is where I create the components that will be rendered.
The content of the file views/SignIn/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const SignIn: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="Dashboard">Go back to Dashboard</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default SignIn;
And the content of the file views/Dashboard/index.tsx is:
import React, { useContext } from 'react';
import Link from 'next/link';
import { AuthContext } from '../../../contexts/auth';
const Dashboard: React.FC = () => {
const { signed, signIn } = useContext(AuthContext);
async function handleSignIn() {
signIn();
}
return (
<div>
<Link href="SignIn">Go back to sign in page</Link>
<button onClick={handleSignIn}>Click me</button>
</div>
);
};
export default Dashboard;
I am able to access the context inside both /Dashboard and /SignIn, but when I press the link, the state comes back to the initial one. I figured out that the whole provider is rerenderized and therefore the new state becomes the initial state, but I wasn't able to go around this issue in a "best practices manner".
If I put the provider inside _app.tsx, I can maintain the state when transitioning between pages, but I end up providing this state to the / route as well, which I am trying to avoid.
I was able to go around this by doing the following, but it really does not seem to be the best solution for me.
I removed the Providers from Pages/SignIn/index.tsx and Pages/Dashboard/index.tsx and used the following snippet for the _app.tsx file:
import React from 'react';
import { AppProps } from 'next/app';
import { useRouter } from 'next/router';
import { AuthProvider } from '../contexts/auth';
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
const router = useRouter();
const AuthProviderRoutes = ['/SignIn', '/Dashboard'];
return (
<>
{AuthProviderRoutes.includes(router.pathname) ? (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
) : <Component {...pageProps} />}
</>
);
};
export default App;
Does anyone have a better solution?

Reactjs - how to pass props to Route?

I’m learning React Navigation using React-Router-Dom. I have created a simple app to illustrate the problem:
Inside App.js I have a Route, that points to the url “/” and loads the functional Component DataSource.js.
Inside DataSource.js I have a state with the variable name:”John”. There is also a buttonwith the onclick pointing to a class method that’s supposed to load a stateless component named ShowData.js using Route.
ShowData.js receives props.name.
What I want to do is: when the button in DataSource.js is clicked, the url changes to “/showdata”, the ShowData.js is loaded and displays the props.name received by DataSource.js, and DataSource.js goes away.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
function App() {
return (
<div className="App">
<Route path='/' component={DataSource}/>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from 'react';
import ShowData from '../components/ShowData'
import {Route} from 'react-router-dom'
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
<Route path='/showdata' render={()=><ShowData name={this.state.name}/>}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
ShowData.js
import React from 'react';
const showData = props =>{
return (
<div>
<p>{props.name}</p>
</div>
)
}
export default showData;
I have tried the following, but, even though the url does change to '/showdata', the DataSource component is the only thing being rendered to the screen:
DataSource.js
showDataHandler = ()=>{
this.props.history.push('/showdata')
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
</div>
)
}
I also tried the following but nothing changes when the button is clicked:
DataSource.js
showDataHandler = ()=>{
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
How can I use a nested Route inside DataSource.js to pass a prop to another component?
Thanks.
EDIT: As user Sadequs Haque so kindly pointed out, it is possible to retrieve the props when you pass that prop through the url, like '/showdata/John', but that's not what I'd like to do: I'd like that the url was just '/showdata/'.
He also points out that it is possible to render either DataSource or ShowData conditionally, but that will not change the url from '/' to '/showdata'.
There were multiple issues to solve and this solution worked as you wanted.
App.js should have all the routes. I used Route params to pass the props to ShowData. So, /showdata/value would pass value as params to ShowData and render ShowData. And then wrapped the Routes with BrowserRouter. And then used exact route to point / to DataSource because otherwise DataSource would still get rendered as /showdata/:name has /
DataSource.js will simply Link the button to the appropriate Route. You would populate DataSourceValue with the appropriate value.
ShowData.js would read and display value from the router prop. I figured out the object structure of the router params from a console.log() of the props object. It ended up being props.match.params
App.js
import { BrowserRouter as Router, Route } from "react-router-dom";
import DataSource from "./DataSource";
import ShowData from "./ShowData";
function App() {
return (
<div className="App">
<Router>
<Route exact path="/" component={DataSource} />
<Route path="/showdata/:name" component={ShowData} />
</Router>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from "react";
import ShowData from "./ShowData";
class DataSource extends Component {
state = {
name: " John",
clicked: false
};
render() {
if (!this.state.clicked)
return (
<button
onClick={() => {
this.setState({ name: "John", clicked: true });
console.log(this.state.clicked);
}}
>
Go!
</button>
);
else {
return <ShowData name={this.state.name} />;
}
}
}
export default DataSource;
ShowData.js
import React from "react";
const ShowData = (props) => {
console.log(props);
return (
<div>
<p>{props.name}</p>
</div>
);
};
export default ShowData;
Here is my scripts on CodeSandbox. https://codesandbox.io/s/zen-hodgkin-yfjs6?fontsize=14&hidenavigation=1&theme=dark
I figured it out. At least, one way of doing it, anyway.
First, I added a route to the ShowData component inside App.js, so that ShowData could get access to the router props. I also included exact to DataSource route, so it wouldn't be displayed when ShowData is rendered.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
import ShowData from './components/ShowData'
function App() {
return (
<div className="App">
<Route exact path='/' component={DataSource}/>
{/* 1. add Route to ShowData */}
<Route path='/showdata' component={ShowData}/>
</div>
);
}
export default App;
Inside DataSource, I modified the showDataHandler method to push the url I wanted, AND added a query param to it.
DataSource.js
import React, { Component } from 'react';
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
this.props.history.push({
pathname:'/showdata',
query:this.state.name
})
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
And, finally, I modified ShowData to be a Class, so I could use state and have access to ComponentDidMount (I guess is also possible to use hooks here, if you don't want to change it to a Class).
Inside ComponentDidMount, I get the query param and update the state.
ShowData.js
import React, { Component } from 'react';
class ShowData extends Component{
state={
name:null
}
componentDidMount(){
this.setState({name:this.props.location.query})
}
render(){
return (
<div>
<p>{this.state.name}</p>
</div>
)
}
}
export default ShowData;
Now, when I click the button, the url changes to '/showdata' (and only '/showdata') and the prop name is displayed.
Hope this helps someone. Thanks.

Resources