JS3
JS3

Reputation: 1829

React - How to stay on the same page even if it was refreshed?

I'm using react-router for the link to the different pages. Everything works fine, however, once I'll refresh the page, it'll go to the login page for a moment and it'll go back to the homepage. It was even worse if I'll go to the admin page, refreshing the page will direct the user to the login page, however, the user is still logged in and only displays the login page. I'm also using Firebase Firestore and firebase authentication.

app.js

const App = (props) => {
  const { setCurrentUser, currentUser } = props;
  const admin = checkUserAdmin(currentUser);
  console.log(admin);

  useEffect(() => {
    const authListener = auth.onAuthStateChanged(async (userAuth) => {
      if (userAuth) {
        const userRef = await handleUserProfile(userAuth);
        userRef.onSnapshot((snapshot) => {
          setCurrentUser({
            id: snapshot.id,
            ...snapshot.data(),
          });
        });
      }

      setCurrentUser(userAuth);
    });

    return () => {
      authListener();
    };
  }, []);

  return (
    <div className="App">
      <Switch>
     
        <Route
          exact
          path="/login"
          render={() => (
            <MainLayout>
              <LoginPage />
            </MainLayout>
          )}
        />
        <Route
          exact
          path="/profile"
          render={() => (
            <WithAuth>
              <MainLayout>
                <ProfilePage />
              </MainLayout>
            </WithAuth>
          )}
        />
        <Route
          exact
          path="/admin"
          render={() => (
            <WithAdmin>
              <AdminHome />
            </WithAdmin>
          )}
        />
      
      </Switch>
    </div>
  );
};

const mapStateToProps = ({ user }) => ({
  currentUser: user.currentUser,
});

const mapDispatchToProps = (dispatch) => ({
  setCurrentUser: (user) => dispatch(setCurrentUser(user)),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);

withAuth - restricting the users for the pages. If currentUser is a guest user, it directs the user to the login page.

import { useAuth } from "./../custom-hooks";
import { withRouter } from "react-router-dom";

const WithAuth = (props) => useAuth(props) && props.children;

export default withRouter(WithAuth);

useAuth - restricting the users for the pages. If currentUser is a guest user, it directs the user to the login page.

const mapState = ({ user }) => ({
  currentUser: user.currentUser,
});

const useAuth = (props) => {
  const { currentUser } = useSelector(mapState);

  useEffect(() => {
    if (!currentUser) {
      props.history.push("/login");
    }
  }, [currentUser]);

  return currentUser;
};

export default useAuth;

withAdmin - pages only accessible to the admin

import { useAdmin } from "../../custom-hooks";

const WithAdmin = (props) => useAdmin(props) && props.children;

export default WithAdmin;

useAdmin - pages only accessible to the admin. If user is not an admin, it directs the user to the login page.

const mapState = ({ user }) => ({
  currentUser: user.currentUser,
});

const useAdmin = (props) => {
  const { currentUser } = useSelector(mapState);
  const history = useHistory();

  useEffect(() => {
    if (!checkUserAdmin(currentUser)) {
      history.push("/login");
    }
  }, [currentUser]);

  return currentUser;
};

export default useAdmin;

Below is my index.js

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

Reducers: userTypes:

const userTypes = {
  SET_CURRENT_USER: "SET_CURRENT_USER",
};

export default userTypes;

userActions:

import userTypes from "./user.types";

export const setCurrentUser = (user) => ({
  type: userTypes.SET_CURRENT_USER,
  payload: user,
});

userReducer:

import userTypes from "./user.types";

const INITIAL_STATE = {
  currentUser: null,
};

const userReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case userTypes.SET_CURRENT_USER:
      return {
        ...state,
        currentUser: action.payload,
      };
    default:
      return state;
  }
};

export default userReducer;

rootReducer:

import { combineReducers } from "redux";

import userReducer from "./user/user.reducer";

export default combineReducers({
  user: userReducer,
});

store.js

import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";

import rootReducer from "./rootReducer";

export const middlewares = [logger];

export const store = createStore(rootReducer, applyMiddleware(...middlewares));

export default store;

checkUserAdmin.js

export const checkUserAdmin = (currentUser) => {
  if (!currentUser || !Array.isArray(currentUser.roles)) return false;
  const { roles } = currentUser;
  if (roles.includes("admin")) return true;

  return false;
};

From the App.js, I console.log(currentUser) and this is what is shows: enter image description here

Upvotes: 3

Views: 4276

Answers (2)

Drew Reese
Drew Reese

Reputation: 202605

I suggest adding an authPending state to your userReducer, initially true, and also set/cleared when the firestore logic is handing user changes.

userReducer & actions

const userTypes = {
  SET_AUTH_PENDING: "SET_AUTH_PENDING",
  SET_CURRENT_USER: "SET_CURRENT_USER",
};

const setAuthPending = pending => ({
  type: userTypes.SET_AUTH_PENDING,
  payload: pending,
});

const INITIAL_STATE = {
  authPending: true,
  currentUser: null,
};

const userReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case userTypes.SET_CURRENT_USER:
      return {
        ...state,
        authPending: false
        currentUser: action.payload,
      };

    case userTypes.SET_AUTH_PENDING:
      return {
        ...state,
        authPending: action.payload,
      };

    default:
      return state;
  }
};

app.js

const App = (props) => {
  const {
    setAuthPending, // <-- access action
    setCurrentUser,
    currentUser
  } = props;

  const admin = checkUserAdmin(currentUser);
  console.log(admin);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged(async (userAuth) => {
      setAuthPending(true); // <-- start auth pending
      if (userAuth) {
        const userRef = await handleUserProfile(userAuth);
        userRef.onSnapshot((snapshot) => {
          setCurrentUser({ // <-- will clear auth pending
            id: snapshot.id,
            ...snapshot.data(),
          });
        });
      } else { 
        setCurrentUser(null); // <-- clear user data and pending
      }
    });

    return () => {
      unsubscribe();
    };
  }, []);

  return (
    <div className="App">
      <Switch>
        ...
      </Switch>
    </div>
  );
};

const mapStateToProps = ({ user }) => ({
  currentUser: user.currentUser,
});

const mapDispatchToProps = {
  setAuthPending, // <-- wrap action creator in call to dispatch
  setCurrentUser,
};

Hooks & Wrappers

For these I suggest abstracting the logic into custom Route components.

const AuthRoute = props => {
  const { authPending, currentUser } = useSelector(state => state.user);

  if (authPending) {
    return "Loading..."; // or maybe a loading spinner
  };

  return currentUser ? (
    <Route {...props} />
  ) : (
    <Redirect to="/login" />
  );
};

const AdminRoute = props => {
  const { authPending, currentUser } = useSelector(state => state.user);

  if (authPending) {
    return "Loading..."; // or maybe a loading spinner
  };

  return checkUserAdmin(currentUser) ? (
    <Route {...props} />
  ) : (
    <Redirect to="/login" />
  );
};

Then the routes become

<Switch>
  <Route
    exact
    path="/"
    render={() => (
      <MainLayout>
        <Homepage />
      </MainLayout>
    )}
  />
  <Route
    exact
    path="/login"
    render={() => (
      <MainLayout>
        <LoginPage />
      </MainLayout>
    )}
  />
  <AuthRoute
    exact
    path="/profile"
    render={() => (
      <MainLayout>
        <ProfilePage />
      </MainLayout>
    )}
  />
  <AdminRoute
    exact
    path="/admin"
    component={AdminHome}
  />
</Switch>

After this, you may want to look into persisting your redux state into localStorage, and repopulating your redux state from localStorage when you are instantiating the store (the preloadedState parameter) object when your app is loading. You can manage yourself or look into something like redux-persist.

Upvotes: 3

hasankzl
hasankzl

Reputation: 1019

When a user login you can store some values about the user in localStorage,like username or a token or just a login ,

localStorage.setItem(IS_LOGIN, true);

After that you can use that in your userReducer, when you initiate state you can directly determine the user is login or not.

const INITIAL_STATE = {
 isLogin: localStorage.IS_LOGIN
};

now you can determine a user is login or not before the page load. If you wanna push user to the login page you can use in useEffect

  useEffect(() => {
    if (!isLogin) {
      props.history.push("/login");
    }
  }, [isLogin]);

  return isLogin;
};

when your app first loaded there is no user information on the userReducer, because of that when page load you will be directing to the login page.

Upvotes: 0

Related Questions