Background to the problem
HOCs
When dealing with Next.Js pages a common practice is to use HOC (Higher-Order Component) to avoid retyping the base of a page.
For example, an authentication HOC can be used to check if a user is authenticated or not. Depending on the outcome the user can either access the page or be redirected to a sign-in page.
Layouts
Another practice that is commonly used by Next.Js programmers is Persistent Layouts. The Persistent layout is an "area" on the page that will not re-render when the user is navigating through pages. This is great for UX (User Experience), for example, the scroll-position of a menu remains on page switch.
Some good links
NextJs.org - Persistent Layout Documentation
CheatCode.co - How to Handle Authenticated Routes (The HOC used below)
The problem
When combining these two practices a problem with persistence occurs. (Read comments inside AuthenticationRoute.js
This is a very simple index.js
import authenticatedRoute from '#components/auth/AuthenticatedRoute';
const App = () => {
return (
<section>
<h1>Logged In</h1>
<h1>App</h1>
</section>
);
};
export default authenticatedRoute(App);
The layout Layout.js
import Link from 'next/link';
import Navbar from '#components/layouts/Navbar';
import SideMenu from '#components/layouts/SideMenu';
function Layout({ children }) {
const baseUrl = '/app';
return (
<section>
<Navbar>
<li>
<Link href={`${baseUrl}/`}>Home</Link>
</li>
<li>
<Link href={`${baseUrl}/test1`}>Test 1</Link>
</li>
<li>
<Link href={`${baseUrl}/test2`}>Test 2</Link>
</li>
</Navbar>
<main>{children}</main>
</section>;
);
}
export default Layout;
And lastly the HOC AuthenticationRoute.js
import { Component as component } from 'react';
import Router from 'next/router';
import Layout from '#components/layouts/app/Layout';
const authenticatedRoute = (Component = null, options = {}) => {
class AuthenticatedRoute extends component {
state = {
loading: true,
};
componentDidMount() {
const isSignedIn = true;
if (isSignedIn) {
this.setState({ loading: false });
} else {
Router.push(options.pathAfterFailure || '/sign_in');
}
}
render() {
const { loading } = this.state;
if (loading) {
return <div />;
}
// This will return the page without the layout
return <Component {...this.props} />;
// Removing the line above and using this instead
// the page now renders with the layout. BUT...
// The layout is not persistent, it will re-render
// everytime a user navigate.
const getLayout = (page) => <Layout>{page}</Layout>;
return getLayout(<Component {...this.props} />);
}
}
return AuthenticatedRoute;
};
export default authenticatedRoute;
Without HOC
Inside index.js this is working when not calling authenticatedRoute:
App.getLayout = getLayout;
export default App;
So my guess is that the authenticatedRoute should return something else then return <Component {...this.props} />;
Related
First, I have my state variable
export const state: State = {
assetPricesInUsd: {
BTC: '20000',
},
supportedAssets: [],
appLoading: true
}
and of course my overmind config as follows:
export const config = merge(
{
state,
actions,
effects,
},
namespaced({
swap,
send,
receive,
wallet,
}),
)
And inside my _app.tsx, since I am using NextJS, I create my app with the Overmind provider as so:
import { config } from '../lib/overmind'
const overmind = createOvermind(config)
function MyApp({ Component, pageProps }: AppProps) {
return (
<OvermindProvider value={overmind}>
<div data-theme='forest'>
<Navbar />
<Component {...pageProps} />
<Toaster position='top-center' />
</div>
</OvermindProvider>
)
}
export default MyApp
And as a super simple example, I created an 'appLoading' property of my state (as seen above) which initializes to true and is set to false at the end of onInitializeOvermind
export const onInitializeOvermind = async ({
effects,
actions,
state,
}: Context) => {
await actions.loadSupportedAssets()
await actions.loadAssetPrices(state.supportedAssets)
console.log('finished initializing')
state.appLoading = false
}
And in my index.tsx page route, I would like the page to respond to the state.appLoading changing like so:
import { useAppState } from 'lib/overmind'
const Home: NextPage = () => {
const { appLoading } = useAppState()
if (appLoading) {
console.log('app is loading')
return <div>loading...</div>
}
return (
<>
<Head>
<title>Create Next App</title>
<meta name='description' content='Generated by create next app' />
<link rel='icon' href='/favicon.ico' />
</Head>
<main className='min-h-screen bg-base grid grid-cols-8 font-Josefin'>
<Sidebar />
<Trade />
</main>
</>
)
}
export default Home
But in my app, <div>Loading...</div> is rendered on initial load, but it is not updated when I set state.appLoading to false.
In the devtools, appLoading is set to true, and the 'finished initializing' is printed to the console, but the page does not rerender.
Am I doing something wrong with accessing the state variable in my Home component, because it seems that the state is not linked to my component, and overmind doesn't know to re-render it?
Edit: There isn’t an issue with creating action/state hooks. My app state changes with actions like state.appLoading, and if I interact with a component THEN its state updates, but if I log in through the Navbar (updating the isConnected state) then the Sidebar component, which uses the isComponent property from the useState hook, doesn’t rerender until I change something about its own internal state.
Edit 2: I made a sample app which highlights the issue I'm facing hosted here: https://github.com/MaxSpiro/example-overmind. This example confirms that the state is being updated, but what I found out is that the two components are not being rendered at first. Only when you change the internal state of the components (with the second button) do they begin to react to the global state of overmind changing.
Edit 3: https://github.com/cerebral/overmind/issues/553
I'm covering app js with layout. I have the sidebar on the left and my pages on the right. But what I want is that the sidebar should not appear on the login page, how can I edit it?
_app.js
Layout.js
you can add a condition with pathname to showing the component or not
something like this:
const router = useRouter():
return (
...
{router.pathname !== '/login' && <Sidebar path={router.route} />}
...
)
If you have some pages that are protected and can be seen by logged in user than you would need Public and Protected Routes and you can show in your Public routes only
If this is not the case then solution mentioned by #kaveh karami is good
I'm thinking you should use Option 4 of the Persistent Layout Patterns article by Adam Wathan (the getLayout Pattern) to implement the layout and pass a prop to conditionally render the sidebar.
In this way, your Login / Register page controls the layout rendering
// Layout.js
export default function Layout({ showSidebar, children, ...rest }){
return (
...
{showSidebar && <Sidebar />}
...
)
}
export function getLayout(page, props) {
return <Layout {...props}>{page}</DefaultLayout>
}
// Login.js
import { getLayout as getDefaultLayout } from './Layout.js'
function Login(){
return (
...
)
}
Login.getLayout = page => getDefaultLayout(page, { showSidebar: true})
I would create a HOC(Higher-Order-Component) called WithSidebar:
import Main from '../../components/Main/Main';
import Sidebar from '../../components/Sidebar/Sidebar';
import classes from './WithSidebar.module.scss';
const WithSidebar = (Component) => {
return props => (
<div className={classes.WithSidebar}>
<Sidebar />
<Main className={classes.Container}>
<Component {...props} />
</Main>
</div>
);
};
export default WithSidebar;
And then export the pages that should include the sidebar like so:
import WithSidebar from '../hoc/WithSidebar/WithSidebar';
const MyPage = () => {
return (...)
}
export default WithSidebar(MyPage)
I find this approach really cleaner than conditionally rendering based on the pathname.
Im learning React with routing and api calls at the moment. I've got an api were I need to find all the urls and post them on a page that was successful. But now I need to make a detail page where I can show more information about the url. Page 1 is like : gather all the urls and show them in a list. And page 2 is if you have clicked on a url in the list on page 1 you will be navigated to page 2 where a detail dialog is showed.
I have the page 1 completely and I can navigate to page 2 but I can't show any data from the api in page 2 information/detailpage.I don't know how to work with the key and get the info. I Tried several things but none of them works. Im using link.Link as key and I need description property , url , api name.
This is my page 1:
import React from "react";
import {Link, NavLink} from 'react-router-dom';
import { Detail } from './Detail';
export default class FetchLinks extends React.Component {
state = {
loading: true,
links: [],
};
async componentDidMount() {
const url = "https://api.publicapis.org/entries?category=development";
const response = await fetch(url);
const data = await response.json();
this.setState({links: data.entries, loading: false});
}
render() {
if (this.state.loading) {
return <div>loading...</div>
}
if (!this.state.links.length) {
return <div>geen links gevonden</div>
}
return (
<div>
{this.state.links.map(link => (
<div key={link.Link}>
<ul><li>
<p>
<NavLink to={`/Detail/${link.Link}`}>
{link.Link}
</NavLink>
</p>
</li></ul>
</div>))}
</div>
);
}
}
this is page 2:
import React from 'react';
import Content from './Content';
const Detail = () =>{
return(
<div>
<h1>Detail Page</h1>
<p>{this.state.links.Link}</p>
</div>
)
}
export default Detail();
This is how it should be
So if I understood correctly, you have all your data in page 1 and need to pass the data to page 2. For this you have to write your NavLink like this:
<NavLink to={{
pathname: `/Detail/${link.Link}`,
state: link.Link
}}>
{link.Link}
</NavLink>
Now you can use UseLocation hook of react-router to access this state that you just passed. So in page 2:
import React from 'react';
import { useLocation } from "react-router-dom";
const Detail = () =>{
const location = useLocation();
return (
<div>
<h1>Detail Page</h1>
<p>{location.state.link}</p>
</div>
);
}
export default Detail();
You can read more about react-router hooks here.
This is my first React Context implementation. I am using Gatsby and in my layout.js I added Context (with objects and handler function) that successfully gets passed to Consumer:
import AppContext from "./AppContext"
const Layout = ({ children }) => {
let doAuthenticate = () => {
authState = {
isAuth: authState.isAuth === true ? false : true,
}
}
let authState = {
isAuth: false,
doAuthenticate: doAuthenticate
}
return (
<>
<AppContext.Provider value={authState}>
<main>{children}</main>
</AppContext.Provider>
</>
)
I successfully execute function in Consumer:
<AppContext.Consumer>
{ value =>
<button onClick={value.doAuthenticate}Sign in</button>
}
</AppContext.Consumer>
I also see the value in doAuthenticate successfully gets updated.
However, another Consumer that is listening to Provider does not update the value. Why?
When you use Gatsby, each instance of the Page will we wrapped with the Layout component and hence you will see that instead of creating one Context that is shared between pages, you end up creating multiple contexts.
Now multiple contexts cannot communicate with each other
The solution here is to make use of wrapRootElement api in gatsby-ssr.js and gatsby-browser.js to wrap all your pages with a single layout component
import React from "react";
import Layout from "path/to/Layout";
const wrapRootElement = ({ element }) => {
return (
<Layout>
{element}
</Layout>
)
}
export { wrapRootElement };
Trying to render state from Context API, but in console it shows as undefined and doesn't render anything.
here is Context file
import React, { useReducer, createContext } from "react"
export const GlobalStateContext = createContext()
export const GlobalDispatchContext = createContext()
const initialState = {
isLoggedIn: "logged out",
}
function reducer(state, action) {
switch (action.type) {
case "TOGGLE_LOGIN":
{
return {
...state,
isLoggedIn: state.isLoggedIn === false ? true : false,
}
}
break
default:
throw new Error("bad action")
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
{children}
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
and here is where value should be rendered
import React, { useContext } from "react"
import {
GlobalStateContext,
GlobalDispatchContext,
} from "../context/GlobalContextProvider"
const Login = () => {
const state = useContext(GlobalStateContext)
console.log(state)
return (
<>
<GlobalStateContext.Consumer>
{value => <p>{value}</p>}
</GlobalStateContext.Consumer>
</>
)
}
export default Login
I tried before the same thing with class component but it didn't solve the problem. When I console log context it looks like object with undefined values.
Any ideas?
The Context API In General
From the comments, it seems the potential problem is that you're not rendering <Login /> as a child of <GlobalContextProvider />. When you're using a context consumer, either as a hook or as a function, there needs to be a matching provider somewhere in the component tree as its parent.
For example, these would not work:
<div>
<h1>Please log in!</h1>
<Login />
</div>
<React.Fragment>
<GlobalContextProvider />
<Login />
</React.Fragment>
because in both of those, the Login component is either a sibling of the context provider, or the provider is missing entirely.
This, however, would work:
<React.Fragment>
<GlobalContextProvider>
<Login />
</GlobalContextProvider>
</React.Fragment>
because the Login component is a child of the GlobalContextProvider.
Related To Gatsby
This concept is true regardless of what library or framework you're using to make your app. In Gatsby specifically there's a little bit of work you have to do to get this to work at a page level, but it's possible.
Let's say you have a Layout.jsx file defined, and the following page:
const Index = () => (
<Layout>
<h1>{something that uses context}</h1>
</Layout>
)
You have 2 options:
The easier option is to extract that h1 into its own component file. Then you can put the GlobalContextProvider in the Layout and put the context consumer in the new component. This would work because the h1 is being rendered as a child of the layout.
Is to do some shuffling.
You might be inclined to put the Provider in the layout and try to consume it in the page. You might think this would work because the h1 is still being rendered as a child of the Layout, right? That is correct, but the context is not being consumed by the h1. The context is being rendered by the h1 and consumed by Index, which is the parent of <Layout>. Using it at a page level is possible, but what you would have to do is make another component (IndexContent or something similar), consume your context in there, and render that as a child of layout. So as an example (with imports left out for brevity):
const Layout = ({children}) => (
<GlobalContextProvider>
{children}
</GlobalContextProvider>
);
const IndexContent = () => {
const {text} = useContext(GlobalStateContext);
return <h1>{text}</h1>;
}
const Index = () => (
<Layout>
<IndexContent />
</Layout>
);