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

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>
);
};

Related

Error - JSX Element type 'X'does not have any constructor or call signatures

I am trying to render a data by context provider however i got this message reading JSX Element type Context does not have any constructor or call signatures.
My Code in App.tsx is
import { Context } from './interfaces/cardContext';
const App = () => {
return (
<div className="App">
<BrowserRouter>
<Navbar />
<Context>
<RestaurantDetails />
</Context>
<Footer />
</BrowserRouter>
</div>
);
};
export default App
and in my Context page
export interface ContextData {
restaurants: restaurantType[];
}
export interface Props {
children: React.ReactNode;
}
export const Context = createContext({} as ContextData);
export const Provider = ({ children }: Props) => {
const [restaurants, setRestaurants] = useState<restaurantType[]>([]);
useEffect(() => {
fetch('http://localhost:5001/restaurants')
.then((res) => res.json())
.then((data) => setRestaurants(data));
}, []);
return (
<Context.Provider value={{ restaurants }}>{children}</Context.Provider>
);
};
You should import your provider as a wrapper of other components:
import { Provider } from './interfaces/cardContext';
const App = () => {
return (
<div className="App">
<BrowserRouter>
<Navbar />
<Provider>
<RestaurantDetails />
</Provider>
<Footer />
</BrowserRouter>
</div>
);
};
export default App
And then use your context like this in other componens:
import {useContext} from 'react';
import { Context } from 'path-to-card-context';
const {restaurants} = useContext(Context);

React with Enzyme test case

import React from 'react';
import PropTypes from 'prop-types';
import { Route } from 'react-router-dom';
import { SelectModal } from 'ux-components';
const ItemSelectRoute = (props) => {
console.log('1111111', props);
return (
<Route
path="/item-select/:label"
render={(routeProps) => (
<SelectModal
isOpen
label={routeProps.match.params.label}
onCloseClick={() => (routeProps.history.push(props.background.pathname))}
/>
)}
/>
);
}
export default ItemSelectRoute;
SelectModal.js
import React from 'react';
import PropTypes from 'prop-types';
import { Dialog } from 'styleguide-react-components';
import ModalHeader from 'ux-components/src/ModalHeader';
import ModalBody from '../../ModalBody/ModalBody';
const SelectModal = ({
onCloseClick, isOpen, itemSummaries,
}) => {
const itemList = itemSummaries;
return (
<Dialog
appearance="lite"
open={isOpen}
title={<ModalHeader header="Please select" />}
type="modal"
hasCloseButton
clickOffToClose
width={750}
onClose={onCloseClick}
>
<ModalBody items={itemList} />
</Dialog>
);
};
export default SelectModal;
I am writing the test case as for ItemSelectRoute
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const state = {
settings: {
configuration: {},
featureToggle: {},
properties: {},
},
};
const store = mockStore(state);
const newProps = {
appData: {
background: {
pathname: '/',
},
};
const wrapper = mount(<ReduxProvider store={store}>
<MemoryRouter initialEntries={['/item-select/test']}>
<Switch>
<ItemSelectRoute
store={store}
dispatch={jest.fn()}
{...newProps}
render={() => (<SelectModal
isOpen
label="track-my-item"
onCloseClick={() => jest.fn()}
/>)}
/>
</Switch>
</MemoryRouter>
</ReduxProvider>);
console.log(wrapper.debug());
When I run the test, I am getting the following error
Cannot read property 'addEventListener' of undefined
I want to write the test case, where if the route is correct, then SelectModal should be present in the elements tree. I tried few options, but I am unable to resolve the error.

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.

Integrate testing for react-query with testing-library

Aim
I am building a NextJS application that uses react-query to fetch data.
I am now trying to implement a testing framework. However, when I run yarn test I get the error below. From the react-query docs, I understand that error typically relates to circumstances where <QueryClientProvider> has not been included in _app.js.
I suspect that I need to introduce some 'mock data' for react-query in index.test.js but haven't been able to find documentation on how to do so.
Error
No QueryClient set, use QueryClientProvider to set one
Code
/tests/index.test.js
import { render, screen } from '#testing-library/react';
import Home from '../pages/index';
describe('Home', () => {
it('renders without crashing', () => {
render(<Home />);
expect(
screen.getByRole('heading', { name: 'Welcome to Next.js!' })
).toBeInTheDocument();
});
});
/pages/index.js
import Link from 'next/link';
import { Button } from 'antd';
import { useQuery } from 'react-query';
import { readUserRole } from '../lib/auth';
import NewBriefButton from '../components/Buttons/NewBriefButton';
import NewJobButton from '../components/Buttons/NewJobButton';
import NewClientButton from '../components/Buttons/NewClientButton';
export default function Home() {
const userRoleQuery = useQuery('userRole', readUserRole);
const { status, data } = userRoleQuery;
if (status === 'error') {
return <p>error...</p>;
}
if (status === 'loading') {
return <p>loading...</p>;
}
return (
<div>
<h1>Home page</h1>
{data === 'Manager' && (
<>
<Button type='primary'>
<Link href='/assets/upload'>Upload Assets</Link>
</Button>
<NewBriefButton />
<NewJobButton />
<NewClientButton />
</>
)}
</div>
);
}
/pages/_app.js
import { QueryClient, QueryClientProvider } from 'react-query';
import { Hydrate } from 'react-query/hydration';
import Layout from '../components/Layout';
import AuthContextProvider from '../context/AuthContext';
import { GlobalStyles } from '../styles';
import 'antd/dist/antd.css';
import 'react-quill/dist/quill.snow.css';
const queryClient = new QueryClient();
export default function MyApp({ Component, pageProps }) {
return (
<AuthContextProvider>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<GlobalStyles />
<Layout>
<Component {...pageProps} />
</Layout>
</Hydrate>
</QueryClientProvider>
</AuthContextProvider>
);
}
As the error suggests, you need to wrap the component you are mounting in your test in a QueryClientProvider as well:
describe('Home', () => {
const queryClient = new QueryClient();
it('renders without crashing', () => {
render(
<QueryClientProvider client={queryClient}>
<Home />
</QueryClientProvider>
);
expect(
screen.getByRole('heading', { name: 'Welcome to Next.js!' })
).toBeInTheDocument();
});
});
I would create a new Provider for each test to keep them isolated.

Why component with Context Provider doesn't re-render

I was looking for the answer why react component with Context Provider doesn't re-render but i couldn't find answer proper for me to understand why.
Moreover i want to mention Im using GatsbyJS.
Here's App.context.js:
const defaultValue = {
menu: false,
handleMenu: () => { },
}
const AppContext = createContext(defaultValue);
export default AppContext;
export { defaultValue };
Next, down below there's Provider element App.provider.js:
import AppContext, { defaultValue } from './App.context';
class AppProvider extends Component {
constructor(props) {
super(props);
this.state = defaultValue
}
handleMenu = () => {
if (this.state.menu) {
this.setState({
menu: false
})
} else {
this.setState({
menu: true
})
}
}
render() {
return (
<AppContext.Provider value={{
menu: this.state.menu,
handleMenu: this.handleMenu,
}}>
{this.props.children}
</AppContext.Provider>
);
}
}
export default AppProvider;
Then, I'm using this provider at the beginning of elements tree:
//Components
import Header from '../components/header';
import Footer from '../components/footer';
import MainWrap from '../components/mainWrap';
//Context
import AppProvider from '../context/App.provider';
const Layout = ({ children }) => {
return (
<AppProvider>
<MainWrap>
<Header />
{children}
<Footer />
</MainWrap>
</AppProvider>
);
}
export default Layout;
Here's MainWrap component:
//Styles
import wrapStyles from '../styles/wrapper.module.scss';
//Context
import AppContext from '../context/App.context';
const MainWrap = ({children}) => {
const {menu} = useContext(AppContext);
return (
<div className={menu?wrapStyles.wrap:wrapStyles.wrapActive}>{children}</div>
);
}
export default MainWrap;
When context value change, child components like MainPage re-render properly, but why component with Provider does not, so i can't instead of using next wrap component (MainPage) just put a div in component with Provider:
//Components
import Header from '../components/header';
import Footer from '../components/footer';
//Styles
import wrapStyles from '../styles/wrapper.module.scss';
//Context
import AppProvider from '../context/App.provider';
import AppContext from '../context/App.context';
const Layout = ({ children }) => {
const {menu} = useContext(AppContext);
return (
<AppProvider>
<div className={menu?wrapStyles.wrap:wrapStyles.wrapActive}>
<Header />
{children}
<Footer />
</div>
</AppProvider>
);
}
export default Layout;
I hope it will be understandable.

Resources