Search code examples
javascriptreactjsreduxnext.js

Access restriction in _app.js with frebase and redux


I have an application where people can access to the website if not login, however they should be able to access to "/app", "/app/*" only if authenticated. My code below works but for some reason there is half a second, or a second of the content of "/app" shown before showing the message "you're not allowed to...". Any idea why is that?

import { Provider, useDispatch, useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import '../styles/front.css';
import '../styles/app.css';
import React, { useEffect, useState } from 'react';
import { wrapper, store } from '../store';
import { login, logout, selectUser } from "../redux/slices/userSlice";
import { auth } from '../firebase';

function MyApp({ Component, pageProps }) {
  const user = useSelector(selectUser);
  const dispatch = useDispatch();
  const router = useRouter();
  const isAppPage = router.pathname.startsWith('/app');
  const [shouldRender, setShouldRender] = useState(true); // New state variable
  const [loading, setLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((userAuth) => {
      if (userAuth) {
        dispatch(login({
          email: userAuth.email,
          uid: userAuth.uid,
          displayName: userAuth.displayName,
          photoUrl: userAuth.photoURL
        }));
        setIsAuthenticated(true);
      } else {
        if (isAppPage) {
          setShouldRender(false);
        }
        dispatch(logout());
        setIsAuthenticated(false);
      }
      setLoading(false); // Set loading to false once the authentication status is checked
    });

    return () => unsubscribe(); // Cleanup the event listener when the component unmounts
  }, []);

  return (
    <Provider store={store}>
      {shouldRender ? <Component {...pageProps} /> : <p>You're not allowed to access that page.</p>}
    </Provider>
  );
}

export default wrapper.withRedux(MyApp);

Solution

  • You basically need a condition with a third value that is neither "show the content" nor "you can't see this content". Something like a "pending" state that conditionally renders neither the Component nor the "You're not allowed to access that page." text.

    The initial shouldRender state matches one of these two states you don't want to immediately render. Start with undefined and explicitly check for this and conditionally return null or a loading indicator, etc. Example:

    function MyApp({ Component, pageProps }) {
      const user = useSelector(selectUser);
      const dispatch = useDispatch();
      const router = useRouter();
      const isAppPage = router.pathname.startsWith('/app');
    
      const [shouldRender, setShouldRender] = useState(); // initially undefined
    
      const [loading, setLoading] = useState(true);
      const [isAuthenticated, setIsAuthenticated] = useState(false);
    
      useEffect(() => {
        const unsubscribe = auth.onAuthStateChanged((userAuth) => {
          if (userAuth) {
            dispatch(login({
              email: userAuth.email,
              uid: userAuth.uid,
              displayName: userAuth.displayName,
              photoUrl: userAuth.photoURL
            }));
            setIsAuthenticated(true);
            setShouldRender(true); // <-- show the content
          } else {
            if (isAppPage) {
              setShouldRender(false); // <-- hide content
            }
            dispatch(logout());
            setIsAuthenticated(false);
          }
          setLoading(false);
        });
    
        return () => unsubscribe();
      }, []);
    
      if (shouldRender === undefined) {
        return null; // or loading indicator/spinner/etc
      }
    
      return (
        <Provider store={store}>
          {shouldRender
            ? <Component {...pageProps} />
            : <p>You're not allowed to access that page.</p>
          }
        </Provider>
      );
    }