NextJS - Passing state to children as props in custom app - reactjs

I have this ./pages/_app.js from the official docs:
import App, { Container } from 'next/app'
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
render () {
const { Component, pageProps } = this.props
return (
<Container>
<Component {...pageProps} />
</Container>
)
}
}
export default MyApp
I want to pass state from MyApp to every page it renders, but can't figure how. I tried this:
const childProps = {
...
}
<Container>
<Component {...childProps} />
</Container>
and got an error:
React.Children.only expected to receive a single React element child.

pass what you want just as you pass usual props
<Component whatyouwant={propsyouwant} />
For example: this is how I passed MobileDetect props to every component in my app:
class MyApp extends App {
static async getInitialProps({ Component, router, ctx }) {
let pageProps = {}
const { req } = ctx
var MobileDetect = require('mobile-detect')
const md = new MobileDetect(req ? req.headers['user-agent'] : "")
const phone = md.phone()
const tablet = md.tablet()
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps,
phone: phone,
tablet: tablet }
}
render () {
const {Component, pageProps, phone, tablet} = this.props
return (
<Container>
<Provider store={reduxStore}>
<Nav />
<Component {...pageProps} phone={phone} tablet={tablet} />
<Footer />
</Provider>
</Container>
)
}
}
export default withReduxStore(MyApp)

The culprit was
<Link>
<Icon />
text
</Link>
in the rendered page (not _app.js).
The Link had two children (remnants of react-router Link) instead of one.

Related

How to Move Next-Redux-Wrapper Store Provider to Layout Component?

Can you please help me move the next-redux-wrapper store provider into a layout component?
My current codes follows the next-redux-wrapper office docs and it works fine, but I would like to move the store provider into a layout component in case where the Redux store provider isn't required as it might just be a plain page.
When I tried to move it to a layout component, I'm not able to access the pageProps as props is now a jsx element. But the parent pageProps is required by next-redux-wrapper.
I don't really know how to do this. How do I get the original parent pageProps in the layout component?
Here is my working version:
//_app.tsx
import { NextPage } from "next";
import type { AppProps } from "next/app";
import { ReactElement, ReactNode } from "react";
import { Provider } from "react-redux";
import { wrapper } from "../../lib/redux/store/store";
import "../../styles/globals.css";
type NextPageWithLayoutAndAuth = NextPage & {
getLayout?: (page: ReactElement) => ReactNode;
auth?: boolean;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayoutAndAuth;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page);
const { store, props } = wrapper.useWrappedStore(pageProps); // <---- Move this to Layout Component
return getLayout(
<Provider store={store}> // <---- Move this to Layout Component
<Component {...props.pageProps} />
</Provider> // <---- Move this to Layout Component
);
}
export default MyApp;
//withLayout.tsx
import AuthLayout from "../components/auth/AuthLayout";
import AuthReduxLayout from "../components/auth/AuthReduxLayout";
import DefaultLayout from "../components/auth/DefaultLayout";
type LayoutType = "default" | "auth" | "authRedux";
export default function withLayout(layoutType: LayoutType, title: string) {
if (layoutType === "auth") {
return function getLayout(page: React.ReactElement) {
return <AuthLayout title={title}>{page}</AuthLayout>;
};
}
if (layoutType === "authRedux") {
return function getLayout(page: React.ReactElement) {
return <AuthReduxLayout title={title}>{page}</AuthReduxLayout>;
};
}
return function getLayout(page: React.ReactElement) {
return <DefaultLayout title={title}>{page}</DefaultLayout>;
};
}
//AuthReduxLayout.tsx
import { QueryClient, QueryClientProvider } from "#tanstack/react-query";
import { ReactQueryDevtools } from "#tanstack/react-query-devtools";
import { Fragment } from "react";
import Footer from "../../../src/components/layout/footer";
import Header from "../../../src/components/layout/header";
const queryClient = new QueryClient();
const AuthReduxLayout = (props: any) => {
// const { store, props } = wrapper.useWrappedStore(pageProps); //<--- Move to here
return (
<QueryClientProvider client={queryClient}>
{/* <Provider store={store}> //<--- Move to here */}
<Fragment>
<Header />
<main>{props.children}</main>
<Footer />
</Fragment>
{/* </Provider> //<--- Move to here */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default AuthReduxLayout;
I tried to access the parent pageProps using React.Children and then apply the next-redux-wrapper's useWrappedStore hook to access the required props, but it doesn't work.
//AuthReduxLayout.tsx (testing version)
import { QueryClient, QueryClientProvider } from "#tanstack/react-query";
import { ReactQueryDevtools } from "#tanstack/react-query-devtools";
import React, { Fragment } from "react";
import { Provider } from "react-redux";
import Footer from "../../../src/components/layout/footer";
import Header from "../../../src/components/layout/header";
import { wrapper } from "../../redux/store/store";
const queryClient = new QueryClient();
const AuthReduxLayout = (parentProps: any) => {
const { store, props } = wrapper.useWrappedStore(parentProps);
const updatedChildren = (pobjProps:any) => {
return React.Children.map(parentProps.children,(child)=> React.cloneElement(child,{ ...pobjProps }))
}
const newChildren = updatedChildren(props);
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Fragment>
<Header />
<main>{newChildren}</main>
<Footer />
</Fragment>
</Provider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
export default AuthReduxLayout;
Any clues would be appreciated. Thanks
I figured it out. I had to pass the page.props in the withLayout hoc as a property of the component.
if (layoutType === "authRedux") {
return function getLayout(page: React.ReactElement) {
return (
<AuthReduxLayout title={title} pageProps={page.props}>
{page}
</AuthReduxLayout>
);
};
}
Then I am able to access that property in the child component like this:
const AuthReduxLayout = (parentProps: any) => {
const { store, props } = wrapper.useWrappedStore(parentProps.pageProps);
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<Fragment>
<Header />
<main>{props.children}</main>
<Footer />
</Fragment>
</Provider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};

Hiding Banner at a certain page

I'm currently attempting to hide the banner at a certain page. I have successfully hid the banner in all other pages except one with a page with a id. I have a dynamic folder named [content]
import { useRouter } from "next/router";
const HIDDEN_BOARDLIST = ["/board/board_list"];
//this is successful
const HIDDEN_BOARDDETAILS = [`board/${content}`].
//this does not work
//http://localhost:3000/board/620471f057aad9002de7f04f. I have to enter the id manually but since this is a dynamic, the id will change every time
export default function Layout(props: ILayoutProps) {
const router = useRouter();
console.log(router.asPath);
const isHiddenBoardList = HIDDEN_BOARDLIST.includes(router.asPath);
return (
<Wrapper>
<Header />
{!isHiddenBoardList && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
useRouter is a hook.
CSR
import React, { useState, useEffect } from 'React';
import { useRouter } from "next/router";
interface ILayoutProps {
//...
}
export default function Layout(props: ILayoutProps) {
const router = useRouter();
const [hidden, setHidden] = useState(false);
useEffect(() => {
if(router.asPath.includes('board/')) {
setHidden(true);
}
}, [router.asPath]);
return (
<Wrapper>
<Header />
{!hidden && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
Since this code is CSR, flickering may occur. <Banner /> will disappear after being rendered.
If you don't want that, there is a way to pass the current url as props of the <Layout /> component via getServerSideProps.
SSR
// pages/board/[id].tsx
import { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
interface Props {
url: string;
}
const BoardPage: NextPage<Props> = (props: Props) => {
return (
<>
<Layout {...props} />
</>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const { resolvedUrl } = context; //ex) /board/12345?id=12345
return {
props: {
url: resolvedUrl ,
}, // will be passed to the page component as props
};
};
// components/Layout.tsx
import React, { useState, useEffect } from 'React';
import { useRouter } from "next/router";
interface ILayoutProps {
url: string;
// ...
}
export default function Layout(props: ILayoutProps) {
return (
<Wrapper>
<Header />
{props.url.includes('board/') && <Banner />}
<BodyWrapper>
<Body>{props.children}</Body>
</BodyWrapper>
</Wrapper>
);
}
I hope these two kinds of code are helpful.

How to use getInitialProps in _app.js file with function component?

I just see code _app.js file like this
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const { token } = parseCookies(ctx);
return { pageProps };
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout {...pageProps}>
<Component {...pageProps} />
</Layout>
);
}
}
export default MyApp;
This is _app.js file
function App({ pageProps, Component }) {
return (
<Layout >
<Component {...pageProps} />
</Layout>
)
}
export default App;
I want to convert first code examle to this. How to do this?
You should be able to set it up as follows, but be aware when you have getInitialProps on your app you are blocking every page, see more details here: https://nextjs.org/docs/advanced-features/custom-app
function App({ pageProps, Component }) {
return (
<Layout >
<Component {...pageProps} />
</Layout>
)
}
App.getInitialProps = async (appContext) => {
// Logic goes here
}
export default App;

empty object with getStaticProps and typescript

I'm trying to fetch some data and via the doc's it seems it has to be via getStaticProps but I keep getting an empty object when logging the props inside my component
if you have any idea why feel free to let me know
here is my _app.tsx
const Child:React.FC = ({ children }) => {
const isDark = useSelector<ThemeState, ThemeState["isDark"]>(state => state.isDark)
return <div className={`${isDark ? "dark blackThemeColor test" : "white whiteThemeColor"}` + " min-h-screen font-body pr-0"} style={{height:'150vh'}}>{children}</div>;
}
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<Provider store={ThemeStore}>
<Application theme={theme}>
<Child>
<Navigation {...pageProps} />
<Component {...pageProps} />
</Child>
</Application>
</Provider>
);
};
export default MyApp
and my navigation.tsx
interface NavProps {
title: string;
body: string;
id: number;
userId: number
}
const Navigation: React.FC<NavProps> = (props: NavProps) => {
useEffect(() => {
console.log(props)
}, [])
console.log(props)
return (
<>
<div className={`w-full h-full `}>
<p>{props.title}</p>
</div>
</>
)
}
export default Navigation
export const getStaticProps: GetStaticProps = async (context) => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=6")
const data: NavProps = await res.json();
console.log(data)
return {
props: {
data
}
}
}
From your comment I take that the navigation.tsx file is not located in the pages folder. To quote the docs:
getStaticProps can only be exported from a page. You can’t export it from non-page files.
So you have to move the function to the page / route where you want to fetch the data or do it clientside.

How to fetch API at component level and prerender it in nextjs?

We need to populate the list of links in the footer component But these methods getInitialProps, getServerSideProps and getStaticProps are not getting executed in the footer component. Here is the code:
const Layout = (props) => {
return (
<>
<Header />
{props.children}
<Footer />
</>
);
}
export default Layout;
--- Footer Component ---
const Footer = ({data}) => {
return(
<ul>
{data && data.map((todo,index)=>{
return <li key={index}>
{todo.title}
</li>
})}
</ul>
);
}
export async function getStaticProps() { // not working with getInitialProps and getServerSideProps as well
const res = await fetch('https://jsonplaceholder.typicode.com/todos')
const data = await res.json();
return {
props: {data}
}
}
export default Footer;
EDIT: Footer component should not displayed in Login/Signup pages.
In the _app.js, i have passed the data as <Component {...pageProps} footerData={...data} /> but the data object is not available in Footer component.
getInitialProps, getServerSideProps, and getStaticProps just only run on top of the page, not component.
If you want to run fetch on Footer, you can call fetch function on useEffect hook, or simply call fetch in one of the getInitialProps, getServerSideProps, and getStaticProps on top of the page (i.e such as _index.js) and pass down Footer by props.
You also can use getInitialsProps on top application in _app.js;
import App, {Container} from 'next/app'
import React from 'react'
import Footer from 'somewhere/Footer';
export default class MyApp extends App {
static async getInitialProps ({ Component, router, ctx }) {
let pageProps = {}
let data = await fetch('/api');
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return {data, pageProps}
}
render () {
const {Component, pageProps, data} = this.props
return <Container>
<Component {...pageProps} footerData={data} />
</Container>
}
}
and FooterComponent look like this
export const Footer = ({data}) => {
return(
<ul>
{data && data.map((todo,index)=>{
return <li key={index}>
{todo.title}
</li>
})}
</ul>
);
}
on the pages except Login, Signup which need Footer component (Home/index.tsx, User/index.tsx)
export const Home = ({footerData}) = {
return <div>
<Header/>
<Footer data={footerData} />
</div>
}

Resources