Reputation: 1829
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:
Upvotes: 3
Views: 4276
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
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