Search code examples
reactjsnext.jsnext-router

Nextjs routing in react - render a page if the user is authenticated


I'm trying to figure out how to set up a nextjs index.tsx page, that renders a page if the user is authenticated and another component if the user is not authenticated.

I can have the not authenticated component rendered properly, but I cannot have the authenticated page rendered correctly. I cant find a tutorial to explain how to put a page in the if statement so that the main nextjs index.tsx page renders the page I specify if there is an authenticated user.

I have an index.tsx in pages with:

import * as React from "react"
import { Box, Center,  Spinner, VStack } from "@chakra-ui/react"
import Head from "next/head"
// import NextLink from "next/link"

import { useMe } from "lib/hooks/useMe"
import { DashLayout } from "components/DashLayout"
import { AuthedHomeLayout } from "components/AuthedHomeLayout"
import LandingPage  from "components/landing/lp"
import { HomeLayout } from "components/HomeLayout"

export default function Home() {
  const { me, loading } = useMe()
  if (loading)
  return (
    <Center>
      <Spinner /> 
    </Center>
  )

  return (
    <Box>
      <Head>
        <title>test</title>
      </Head>

     
        <Center flexDir="column" w="100%">
          <VStack>
            
            {me?  <AuthedHomeLayout><DashLayout /></AuthedHomeLayout> : (
              <HomeLayout><LandingPage /></HomeLayout>
            )}
            
          </VStack>
        </Center>
      
    </Box>
  )
}

When I try this as an authenticated user, the DashLayout does load, but the links in it do not render.

The DashLayout has a set of links in it that form the pages of the dashboard:

import * as React from "react"
import { Box, Flex, Heading, Link, LinkProps, Stack, useColorModeValue } from "@chakra-ui/react"
import NextLink from "next/link"
import { useRouter } from "next/router"

const DashLayout: React.FC = ({ children }) => {
  return (
    <Box pt={10} pb={20} w="100%">
      
      <Flex flexWrap={{ base: "wrap", md: "unset" }}>
        <Box pos="relative">
          <Stack
            position="sticky"
            top="100px"
            minW={{ base: "unset", md: "200px" }}
            mr={8}
            flexDir={{ base: "row", md: "column" }}
            mb={{ base: 8, md: 0 }}
            spacing={{ base: 0, md: 4 }}
          >
            <ProfileLink href="/dash">Dashboard</ProfileLink>
            <ProfileLink href="/dash/library">Library</ProfileLink>
            <ProfileLink href="/dash/help">Help</ProfileLink>
            
            
          </Stack>
        </Box>
        <Box w="100%">{children}</Box>
      </Flex>
    </Box>
  )
}

export default DashLayout




interface ProfileLinkProps extends LinkProps {
  href: string
}
const ProfileLink: React.FC<ProfileLinkProps> = ({ href, ...props }) => {
  const { asPath } = useRouter()
  const isActive = asPath === href
  const activeColor = useColorModeValue("black", "white")
  const inactiveColor = useColorModeValue("gray.600", "gray.500")
  return (
    <NextLink href={href} passHref>
      <Link
        pr={4}
        h="25px"
        justifyContent={{ base: "center", md: "flex-start" }}
        textDecoration="none !important"
        color={isActive ? activeColor : inactiveColor}
        _hover={{ color: useColorModeValue("black", "white") }}
        fontWeight={isActive ? "semibold" : "normal"}
      >
        {props.children}
      </Link>
    </NextLink>
  )
}

The page I want to render if there is an auth user, is:

import * as React from "react"
import { gql } from "@apollo/client"
import { Center, Spinner, Stack, Text } from "@chakra-ui/react"

import { useUpdateMeMutation } from "lib/graphql"
import { useForm } from "lib/hooks/useForm"
import { useMe } from "lib/hooks/useMe"
import { useMutationHandler } from "lib/hooks/useMutationHandler"
import { UPLOAD_PATHS } from "lib/uploadPaths"
import Yup from "lib/yup"
import { ButtonGroup } from "components/ButtonGroup"
import { Form } from "components/Form"
import { withAuth } from "components/hoc/withAuth"
import { AuthedHomeLayout } from "components/AuthedHomeLayout"
import { ImageUploader } from "components/ImageUploader"
import { Input } from "components/Input"
import { DashLayout } from "components/DashLayout"

const _ = gql`
  mutation UpdateMe($data: UpdateUserInput!) {
    updateMe(data: $data) {
      ...Me
    }
  }
`

const ProfileSchema = Yup.object().shape({
  email: Yup.string().email().required("Required").nullIfEmpty(),
  firstName: Yup.string().required("Required").nullIfEmpty(),
  lastName: Yup.string().required("Required").nullIfEmpty(),
})
function Dash() {
  const { me, loading } = useMe()

  const handler = useMutationHandler()
  const [updateUser] = useUpdateMeMutation()

  const updateAvatar = (avatar: string | null) => {
    return handler(() => updateUser({ variables: { data: { avatar } } }), {
      onSuccess: (_, toast) => toast({ description: "Avatar updated." }),
    })
  }

  const defaultValues = {
    email: me?.email || "",
    firstName: me?.firstName || "",
    lastName: me?.lastName || "",
  }

  const form = useForm({ defaultValues, schema: ProfileSchema })

  const handleUpdate = (data: typeof defaultValues) => {
    return form.handler(() => updateUser({ variables: { data } }), {
      onSuccess: (_, toast) => {
        toast({ description: "Info updated!" })
        form.reset(data)
      },
    })
  }

  if (loading)
    return (
      <Center>
        <Spinner />
      </Center>
    )
  if (!me) return null
  return (
    <Stack spacing={6}>
      <Tile>
        <Text>alskjf</Text>
      </Tile>
      
    </Stack>
  )
}

Dash.getLayout = (page: React.ReactNode) => (
  <AuthedHomeLayout>
    <DashLayout>{page}</DashLayout>
  </AuthedHomeLayout>
)

export default withAuth(Dash)

I also tried defining the index.tsx condition as:

                {me? 
<Dash /> // Dash is defined as a page in the pages folder at dash/index
 ///<AuthedHomeLayout><DashLayout /></AuthedHomeLayout> 
: (
                  <HomeLayout><LandingPage /></HomeLayout>
                )}

How can I have index.tsx defined to render one page if there is an authed user and another if there is not?

I saw this post and tried using one of the suggestions it makes, as follows:

import Router from 'next/router';

{me?  Router.push('/dash') : (
              <HomeLayout><LandingPage /></HomeLayout>
            )}

When I try this, I get errors that read:

[{
    "resource": "/src/pages/index.tsx",
    "owner": "typescript",
    "code": "2322",
    "severity": 8,
    "message": "Type 'Element | Promise<boolean>' is not assignable to type 'ReactNode'.\n  Type 'Promise<boolean>' is not assignable to type 'ReactNode'.",
    "source": "ts",
    "startLineNumber": 32,
    "startColumn": 13,
    "endLineNumber": 34,
    "endColumn": 15,
    "relatedInformation": [
        {
            "startLineNumber": 1360,
            "startColumn": 9,
            "endLineNumber": 1360,
            "endColumn": 17,
            "message": "The expected type comes from property 'children' which is declared here on type 'IntrinsicAttributes & OmitCommonProps<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, keyof StackProps> & StackProps & { ...; }'",
            "resource": "repo/node_modules/@types/react/index.d.ts"
        }
    ]
}]

Solution

  • In the solutions you tried, the last one was almost correct. You were on the right path, that you should redirect the user to the /dash page if he is authenticated. But you were doing the redirection in the return statement of your component, which is not where you want to do any side effect logic.

    Your attempt:

    import Router from 'next/router';
    
    {me?  Router.push('/dash') : (
       <HomeLayout><LandingPage /></HomeLayout>
    )}
    

    will not work because Router.push returns a <Promise<boolean>>.

    Don't forget that React components must return React elements. In your case when the user is authenticated, you are returning a promise not a React element.

    So your redirection (which is a side effect) should be done inside a useEffect hook.

    In order to fix this, Next documentation provides a clear example of how to do it correctly. What you are looking for is the last code block of this section (the one just before this section).

    Don't forget to use a valid router instance via the useRouter hook provided by next/router.

    So your code now becomes something like:

    import { useEffect } from 'react';
    import { useRouter } from 'next/router';
    
    // Whatever Component you were doing the redirect
    const export YourComponent = () => {
        // your component hooks and states
        const { me, loading } = useMe();
        const router = useRouter();
    
        // Here is what you were missing
        useEffect(() => {
            if (me) {
                router.push('/dash');
            }
        }, [me]);
    
        // you can add a loader like you did before
        return loading ? (
            <Center><Spinner /></Center>
        ) : (
            <HomeLayout><LandingPage /></HomeLayout>
        );
    };
    

    It should be enough to get to what you're looking for.

    As a side note, your first solution:

    {me? 
        <Dash /> // Dash is defined as a page in the pages folder at dash/index
        ///<AuthedHomeLayout><DashLayout /></AuthedHomeLayout> 
    : (
        <HomeLayout><LandingPage /></HomeLayout>
    )}
    

    cannot work, as <Dash /> is a Next Page which is associated with a route based on its file name. You can look at it like an entry point.