I just started using auth0 sdk for nextjs projects. Everything seems to be ok, but I have one little problem. Everytime I change route (while I am logged in) there is invoked the /me api. This means that the user that I got through useUser ctx becomes undefined and there is a little flash of the pages rendered only if logged in.
This is the general structure of my project
_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { appWithTranslation } from "next-i18next";
import { UserProvider } from "#auth0/nextjs-auth0/client";
import AppMenu from "../components/AppMenu";
function MyApp({ Component, pageProps }: AppProps) {
return (
<UserProvider>
<AppMenu>
<Component {...pageProps} />
</AppMenu>
</UserProvider>
);
}
export default appWithTranslation(MyApp);
AppMenu is a component that I render only if I have the user. It's something like:
import { NextPage } from "next";
import { useUser } from "#auth0/nextjs-auth0/client";
interface Props {
children: JSX.Element;
}
const AppMenu: NextPage<Props> = ({ children }) => {
const { user } = useUser();
return (
<div>
{user && (<nav>{/* more stuff...*/}</nav>) }
{children}
</div>
);
};
export default AppMenu;
In this component I have the app's menu.
As I said before, when I switch from a route to another I can see in the network tab that there is called the /me endpoint and "user" is undefined. This implies that the app's menu (and all the protected components) is not rendered and there is a nasty flash of the page waiting for the response.
Is there a way to fix this? I was looking for a isLoggedIn prop but I haven't found anything.
What do you suggest?
p.s. Possibly, I would avoid a "loading" component
useUser() is a hooks call. Therefore, you need to use useEffect() with a Promise and wait until load the user. However, by the end of the day loading component is a must.
In the meantime, A feature called isLoading comes up with the useUser() state. You can improve your code like the below.
const { user, error, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
Source
Related
I'm taking my first pass at using NextJS, and also my first pass at using a headless CMS (DatoCMS in this case).
Everything was actually working fine, but I found myself using a lot of prop drilling, to get information returned from the CMS down into deeply nested components, and that was becoming cumbersome. After some googling, I decided to try to use React Context to avoid prop drilling as described here.
The first use case is getting site branding info and menu items down to the nav component (this info will almost always be the same, everywhere on the site, but there might be some sections (e.g., landing pages) where they're different).
The problem is, the React useState set function as passed through the ContextProvider doesn't actually seem to set the state. There are several questions on here about state not being updated in various scenarios, but this isn't setting the initial state after NextJS grabs the data from getStaticProps, but here it's not doing it even if I manually call the set function directly in the component wrapped in ContextProvider, never mind somewhere farther down the stack.
As described in that blog post, I have the context provider pulled out into its own component:
// context/header.js
import { createContext, useContext, useState } from 'react';
const HeaderContext = createContext();
export function HeaderProvider({children}) {
const [header, setHeader] = useState(null);
const value = { header, setHeader };
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Then, in _app.js, I wrap my <Header /> component in the context provider:
// pages/_app.js
import Header from '../components/Header';
import { HeaderProvider } from '../lib/context/header';
function MyApp({ Component, pageProps }) {
return (
<>
<HeaderProvider>
<Header { ...pageProps } />
</HeaderProvider>
<Component {...pageProps} />
</>
)
}
export default MyApp
I'm just starting this out, so right now, everything is designed to be a static page (no server side rendering or client side React yet).
The main page, from index.js, successfully grabs the data from DatoCMS:
// pages/index.js
import { request } from "../lib/datocms";
import { HOMEPAGE_QUERY } from '../lib/query';
export async function getStaticProps() {
const data = await request({
query: HOMEPAGE_QUERY
});
return {
props: { data }
};
}
export default function Home({ data }) {
return (
<div>
This is the home page
</div>
);
}
So far, this is working, because in my <Header /> component, I can log the data passed.
As I understand how NextJS works, that getStaticProps call populates pageProps which get passed back to the Component (the page) to generate the static html. The results of the request call in getStaticProps is in fact making it's way back to pageProps to be used both in the <Component /> and the <Header /> in _app.js, but when I try to use the setHeader() function from header.js, pulled in by calling useHeader(), the value of the stateful header is always null.
// components/Header.js
import React, { useEffect } from 'react'
import { useHeader } from '../lib/context/header';
import Navbar from './Navbar';
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log(HeaderData);
// outputs:
// {
// siteName: 'My Site',
// companyLogo: {
// alt: 'My Site Logo',
// height: 113,
// url: 'https://www.datocms-assets.com/<url>',
// width: 122
// },
// menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
// }
const { setHeader } = useHeader();
useEffect(() => {
setHeader(HeaderData)
}, [HeaderData]);
return (
<header>
<Navbar />
</header>
)
}
And then using the context in <Navbar />:
import React from 'react';
import { useHeader } from '../lib/context/header';
export default function Navbar() {
const { header } = useHeader();
console.log(header); // null :(
return (
<div>This is my nav</div>
)
}
How do I actually get my context state into <Navbar />?
The problem is, the header context in <Navbar /> is always the initial state of null. There was a similar question, but it wasn't using NextJS, just React, and the answer there seemed to be that he was trying to use the useContext call outside of the component chain he had wrapped in ContextProvider. Maybe I'm missing something, but I'm pretty sure that's not my issue.
So, I did the natural debugging thing, and started adding a bunch of console.logs. Before I even get to the <Navbar /> component, it appears that the setHeader() call isn't actually updating the state.
Here's an updated <Header />:
export default function Header({data}) {
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
console.log('Set HeaderData to');
console.log(HeaderData);
// In the initial code, I just pulled in setHeader here, but now pulling in header for debugging purposes
const { header, setHeader } = useHeader();
setHeader(HeaderData); // various incantations here
console.log('Just set header, header is');
console.log(header)
But no matter what incantation I use at "various incantations here", the result logged to console is always the same:
Set HeaderData to
{
siteName: 'My Site',
companyLogo: {
alt: 'My Site Logo',
height: 113,
url: 'https://www.datocms-assets.com/<url>',
width: 122
},
menu: { title: 'primary', menuItems: [ [Object], [Object] ] }
}
Just set header, header is
null
I've tried setHeader(HeaderData), which seems most analogous to what he did in that original blog post, but since that wasn't working, I also tried setHeader({HeaderData}) and setHeader({...HeaderData}), but the results are identical.
Why isn't this set setting?
There are a few issues with the code as written. Firstly you shouldn't try to console log the new state value after setting it. It won't work. Calling setHeader doesn't change the value of header in this call. It causes it to be changed for the next rendering of the component (which will happen immediately).
Secondly, don't use useEffect to synchronize your state.
const HeaderData = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
const { setHeader } = useHeader();
useEffect(() => {
setHeader(HeaderData)
}, [HeaderData]);
This code fragment used in any component which is an ancestor of a HeaderContext will cause an infinite render loop. The useEffect will fire each time HeaderData changes. But HeaderData is constructed each time the component is rendered. So the useEffect will fire each time the component is rendered, and it will call setHeader which will force a re-render, which closes the loop.
If you're trying to simply specify the initial state (and not trying to update it in response to some event), simply pass the initial state to the provider component. e.g.
// context/header.js
import { createContext, useContext, useState } from 'react';
const HeaderContext = createContext();
const defaultHeaderState = {
siteName: data.siteBranding.siteName,
companyLogo: data.siteBranding.companyLogo,
menu: data?.menu
};
export function HeaderProvider({
initialState = defaultHeaderState,
children
}) {
const [header, setHeader] = useState(initialState);
const value = { header, setHeader };
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Chad's answer was definitely right, directionally — don't use useEffect, and "just set it". The problem was,
how to set it from data coming in via getStaticProps?
The answer was to extract it at the _app.js level from pageProps, and pass it directly as a value to the context provider. In fact, there's no longer even a reason to use setState:
// pages/_app.js
import { HeaderProvider } from '../lib/context/header';
function MyApp({ Component, pageProps }) {
// pull the initial data out of pageProps
const headerData = {
urgentBanner: pageProps.data?.urgentBanner,
siteName: pageProps.data.siteBranding.siteName,
companyLogo: pageProps.data.siteBranding.companyLogo,
menu: pageProps.data?.menu
}
return (
<>
{ /* pass it as a value to the context provider */ }
<HeaderProvider value={headerData}>
<Header { ...pageProps } />
</HeaderProvider>
<Component {...pageProps} />
</>
)
}
and the context provider component gets simplified down to
// context/header.js
import { createContext, useContext } from 'react';
const HeaderContext = createContext();
export function HeaderProvider({value, children}) {
return (
<HeaderContext.Provider value={value}>
{children}
</HeaderContext.Provider>
)
}
export function useHeader() {
return useContext(HeaderContext);
}
Finally, everywhere you want to use it (in a descendent of the context provider, HeaderProvider), you can just:
import { useHeader } from '../lib/context/header';
const headerData = useHeader();
I'm currently trying to develop an app with multiple screens. Specifically, I'm working on the navigator component that directs the user to the login screen or the home screen based on whether they are logged in or not.
To do this, I'm making use of hooks, React Navigation and Firebase. I have a state which tracks the user, and this state is updated using onAuthStateChanged() from Firebase, which is inside a useEffect hook.
import { useState, useEffect } from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import {
HomeScreen,
LoginScreen,
TimerScreen
} from '../screens';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';
const MainStack = createNativeStackNavigator();
const AppNavigator = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const subscriber = onAuthStateChanged(auth, authUser => {
if (authUser) {
setUser(authUser);
} else {
setUser(null);
}
});
return subscriber;
});
const MainNavigator = () => (
...
);
return (
<NavigationContainer>
{ user ? MainNavigator() : LoginScreen() }
</NavigationContainer>
);
};
export default AppNavigator;
AppNavigator is then called in my App.js:
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<AppNavigator />
</View>
);
}
However, whenever I run the app, I get
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I've read a few posts with the same error message, and a common recommendation is to avoid having hooks inside conditional statements / loops. I did check that my useState and useEffect were at the top level of my component, so that doesn't seem to be the issue.
Right now I'm thinking that the problem could be arising because I'm navigating between screens, but I'll have to look more into it though.
Does anyone know what might be the issue, or any other possible fixes I could try? Any help would be great. Thanks!
user ? MainNavigator() : LoginScreen()
You are calling components as regular functions instead of creating elements from them. To create an element, use the JSX syntax, i.e.:
user ? <MainNavigator /> : <LoginScreen />
(Which will then be transpiled to React.createElement.)
The error occurs because when calling these components as functions, the code inside becomes a part of the render phase of the AppNavigator component. If, for example, MainNavigator contains hooks, and LoginScreen does not, then toggling between which function is (incorrectly) called also changes the number of hooks rendered, as suggested in the error message.
I have three pages that I want to share data between (these are the core of the web app) but I also have a bunch of blog pages that don't care about that data. Everywhere I've looked suggests putting the Provider in the _app.tsx file. If I understand that correctly if I wrapp MyApp with the provider, if someone goes straight to www.myurl.com/blog/my-blog-1 (from google), that will cause the provider to run its functions; even if that page won't call useContext
How do I only wrap three pages in the Provider and leave out the blog pages?
For example:
www.myurl.com -> I want this to use the shared data from the provider
www.myurl.com/my-functionality -> I want this to use the shared data from the provider
www.myurl.com/profile-> I want this to use the shared data from the provider
www.myurl.com/blog/* -> I don't want this to call the functions in the auth provider
Here's what my _app.tsx looks like:
import { AppProps } from 'next/app'
import '../styles/index.css'
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
A bit late to the party but there is actually an official way to do this:
Use a per page layout.
First slightly tweak your _app.tsx file:
import type { ReactElement, ReactNode } from 'react';
import { AppProps } from 'next/app';
import type { NextPage } from 'next';
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export const App = ({ Component, pageProps }: AppPropsWithLayout): unknown => {
// Use the custom layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
};
Then in a Page component write the following:
// DashboardPage.ts that need to have a UserProvider and CompanyProvider
import Layout from '#components/Layout'; // Default Layout that is imported for all the page
import { ReactElement } from 'react';
import { UserProvider } from '#contexts/user';
import { CompanyProvider } from '#contexts/company';
const DashboardPage = (): JSX.Element => {
return <DashboardContainer />;
};
// Custom Layout to wrap the page within the User and company providers
DashboardPage.getLayout = function getLayout(page: ReactElement) {
return (
<Layout title="Dashboard page">
<UserProvider>
<CompanyProvider>{page}</CompanyProvider>
</UserProvider>
</Layout>
);
};
export default DashboardPage;
Well, this is an interesting issue.
Challenge is in the way Next implements file-based routing. Since there is no way to create a wrapper for the group of pages only out-of-the-box thing you can do is to wrap the App in the context providers. But that doesn't really resolve your issue.
SOOO... I think there is a workaround. If you want to be able to wrap a certain group of pages in the context provider, first, you need to replace the file-based router with the react-router.
There is a very interesting article on this topic by Andrea Carraro. I think you should try this out:
https://dev.to/toomuchdesign/next-js-react-router-2kl8
I will try to find another solution as well, but let me know did this worked for you.
There is workaround to wrap components of a specific path with a wrapper without react-router.
Check out this video:
https://www.youtube.com/watch?v=69-mnojSa0M
The video shows you how to wrap components of a sub-path with a nested layout. Create a nested layout and wrap that that layout with your Context Provider.
I'm kind new to Next.js and looking for the same question.
Maybe we can just use pathname and query from the useRouter hook and try some conditionals in our Contexts or in the custom _app
next/router
Very late to answer my question, but my approach might help someone in the future.
I used a page as a wrapper, and to navigate around I used query params.
For example
www.something.com/blog
wwww.somethiing.com/blog?id=1
www.something.come/blog?profileid=2
I use contextProvider and check the path pages in _app.js
myContextProvider.js
import React, { useState } from 'react';
const Context = React.createContext({});
export function myContextProvider({ children }) {
const [ state1, setState1 ] = useState(true);
return <Context.Provider value={{ state1, setState1 }}>
{children}
</Context.Provider>;
}
export default Context;
_app.js
import { useRouter } from 'youreFavoRouter';
import { myContextProvider } from './myContextProvider.js';
export default function MyApp({ Component, pageProps }) {
const router = useRouter();
const isBlogPage = router.pathname.includes('blog');
return (
<div>
{isBlogPage ? (
<Component {...pageProps} />
) : (
<myContextProvider>
<Component {...pageProps} />
</myContextProvider>
)}
</div>
);
}
Now all states and function on myContextProvider can see and use on pages than you choose.
I hope help anyone. :)
Im trying to make my privateRoute wait for my API calls. to determine if the user is online and allowed to the page or not. but im getting the following error:
error:
Error: PrivateRoute(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
i think its not waiting for a response to render since im waiting for my server. How do i solve so it wait?
im calling it like this:
server response privateroute:
import React from 'react';
import PropTypes from 'prop-types';
import { Route, Navigate } from 'react-router-dom';
import { useNavigate } from "react-router-dom";
import Login from './components/login/login.jsx';
import GameComponent from './gameComponent.jsx';
import axios from 'axios';
const PrivateRoute = ({ component: Component, redirectTo, isAuth, path, ...props }) => {
//isAuth = false;
axios.post(`http://localhost:3000/online`,{withCredentials: true})
.then(res => {
console.log(res);
if (res) {
isAuth = true;
} else {
isAuth = false;
}
if(!isAuth) {
return <Navigate to={redirectTo} />;
}
return <Route path={path} element={<Component />} />
});
};
export default PrivateRoute;
old code:
const PrivateRoute = ({ component: Component, redirectTo, isAuth, path, ...props }) => {
//isAuth = false;
// no code checking... or real auth
if(!isAuth) {
return <Navigate to={redirectTo} />;
}
return <Route path={path} element={<Component />} />
};
export default PrivateRoute;
update: Remember it has to work with React-route Version 6
You will find my intake here : https://codesandbox.io/s/react-router-basic-forked-m0624?file=/private.js (see the private.js file, I made a private route for the topic route (topics button))
Your first mistake is that your PrivateRoute component is not returning anything. You would have to do return axios.post to return at least something (that what gives the error).
Then as axios.post is an async function that takes some time to resolve, nothing is rendered from PrivateRoute while it is fetching so you have to make sure that PrivateRoute is at least returning a loading component while it fetches.
if (isLoading) return <div>loading</div>;
Then in your following code, React wont do anything if you change variable like that. (so it won't "react" to the change)
isAuth = true;
} else {
isAuth = false;
}
You have to make a hook function like this let [isAuth, setIsAuth] = React.useState(false) and changing the variable like this setIsAuth(true). In my solution I made a custom hook for isAuth and isLoading to get and set the variables and so that React can react to their change and render the good component.
I am using OIDC redux connector for user state. I have a few components that require authentication. I would like to use something like export default connect(mapStateToProps, mapDispatchToProps)(withAuth(Component)); and request data from state inside my authentication service.
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { push } from 'connected-react-router'
export const withAuth = (Component) => {
return props => {
return <Component {...props} />
}
}
Is it possible to get state in the render function? So I can check the user beinig logged in and redirect to the sign-in page if there is no user signed in?
BTW: How would I redirect? I have tried using redirect-router-dom and <Redirect /> But then it complains about set state being changed too often ... But that might be my mistake. I get this error when I render a Redirect: Error: Maximum update depth exceeded.
If I understand correctly you want to "decorate" your component with additional logic to handle an authentication redirect?
I suggest using a "decorator" pattern here e.g.:
export const withAuth = (Component) => {
const mapStateToProps = (state, ownProps) => ({
authenticated: state.authenticated // Or what you need to do to determine this
});
return connect(mapStateToProps)(class extends React.Component {
render() {
const { authenticated, ...componentProps } = props;
if (authenticated) {
return <Component {...componentProps }>;
}
return <Redirect to="/login" />;
}
});
}
Then when you need this you can do things like:
export default withAuth(connect(yourOwnMapStateToProps)(YourComponent))
Just figured it out, I changed the store so instead of returning a function, it returns the object. So I can load in all js files. It might not be the best solution. If there is a better way to get the store in code, I would love to hear about how to do that. The configurestore function is what I found in quite a lot of examples.
import { store } from '../configureStore';
Using store.getState() I can get the current state.
The redirect issue I am having is similar to: How to use react-transition-group with react-router-dom