I'm trying to implement protected pages with Firebase authentication. I created a typical PrivateRoute component that is supposed to only show the page if they're logged in, or redirect users to a login page if they aren't logged in.
I stored the authentication status in a global state using a useEffect hook in App.js. After a lot of reading and research, I understand that the useEffect only completes after the Child component has loaded.
Having said that, I'm at a loss on how to pass authenticated from App to PrivateRoute. With my current code, the authenticated state only registers as true after PrivateRoute has pushed users to the login page. Would appreciate any help.
App.js
//context
const [{user, authenticated}, dispatch] = useStateValue();
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
console.log("THE USER IS >>> ", authUser);
if (authUser) {
dispatch({
type: "SET_USER",
user: authUser,
});
dispatch({
type: "SET_AUTH",
authenticated: true,
})
} else {
// the user is logged out
dispatch({
type: "SET_USER",
user: null,
});
dispatch({
type: "SET_AUTH",
authenticated: false,
})
}
});
}, []);
return (
<Router>
<div className="app">
<PrivateRoute exact isAuth={authenticated} path="/account/create-store" component={CreateAccount} />
</div>
</Router>
)
PrivateRoute.js
import { Route, Redirect } from 'react-router';
import { useStateValue } from './StateProvider';
function PrivateRoute ({ isAuth: isAuth, component: Component, ...rest }) {
const [{authenticated}, dispatch] = useStateValue();
return (
<Route {...rest} render={(props) => {
if (isAuth) {
return <Component />
} else {
return (
<Redirect to={{ pathname:"/login", state: {from: props.location }}} />
);
}
}} />
)
}
export default PrivateRoute
reducer.js
export const initialState = {
user: null,
authenticated: false,
};
const reducer = (state, action) => {
console.log(action)
switch(action.type) {
case "SET_USER":
return {
...state,
user: action.user,
}
case "SET_AUTH":
return {
...state,
authenticated: action.authenticated,
}
default:
return state;
}}
export default reducer;
I can't reproduce your exact problem but I think your PrivateRoute is wrong. Try something like the example bellow.
function PrivateRoute({ isAuth, component: Component, ...rest }) {
if (!isAuth) {
return <Redirect to={{ pathname:"/login", state: {from: props.location }}} />;
}
return <Route component={Component} {...rest} />;
}
export default PrivateRoute;
Use a isLoading state variable so you are not redirected before checking the firebase.auth().onAuthStateChanged.
const [{user, authenticated}, dispatch] = useStateValue();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
console.log("THE USER IS >>> ", authUser);
if (authUser) {
dispatch({
type: "SET_USER",
user: authUser,
});
dispatch({
type: "SET_AUTH",
authenticated: true,
})
} else {
// the user is logged out
dispatch({
type: "SET_USER",
user: null,
});
dispatch({
type: "SET_AUTH",
authenticated: false,
})
}
setIsLoading(false);
});
}, []);
if(isLoading) {
return <div>Loading...</div>
}
return (
<Router>
<div className="app">
<PrivateRoute exact isAuth={authenticated} path="/account/create-store" component={CreateAccount} />
</div>
</Router>
)
Related
I'm trying to set up a simple authentication system using react's context api. I have two react pages here using react router, Login.js and App.js.
Here's App.js, I want it to use the isAuthenticated boolean from the context api to decide which page to render:
App.js:
function App() {
const { isAuthenticated } = useContext(AuthContext);
return (
<div className="App">
<AuthContextProvider>
<Router>
<Routes>
<Route path="/" element={isAuthenticated ? <Home /> : <Login />} />
<Route path="/register" element={isAuthenticated ? <Home /> : <Register />} />
</Routes>
</Router>
</AuthContextProvider>
</div>
);
}
Here's how login.js authenticates the user:
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { dispatch } = useContext(AuthContext);
const handleClick = (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const credentials = {
email: email,
password: password,
}
//Make login request to the server, and use the userID it sends back to authenticate the user
//Currently has a bug where userID is blank on first call, but works on later calls
axios.post('/auth/login', credentials)
.then(response => {
dispatch({ type: "LOGIN_SUCCESS", payload: response.data });
// console.log(userID);
// console.log(isAuthenticated);
})
.catch(err => {
console.log(err)
dispatch({ type: "LOGIN_FAILURE", payload: err });
});
}
catch(err) {
console.log(err);
}
}
Now there are 2 issues here:
In login.js, when I click the login button that calls handleClick, and I do console.log(isAuthenticated), it logs false the first time. Any time after that it will log true.
In App.js, isAuthenticated never changes to true, even while login.js will console.log it as true, so the user is never brought to the home page.
I've been struggling with this for a while now, and I just can't figure out what's going wrong here. I think it may have something to do with App.js not rerendering after the user logs in but I'm not sure.
I also have three files handling the authorization context. I don't think these are causing the issue but I will provide the code just in case I'm missing something
AuthContext.js:
import { createContext, useReducer } from "react";
import AuthReducer from "./AuthReducer";
const initialState = {
userID: null,
isAuthenticated: false,
error: false,
}
export const AuthContext = createContext(initialState);
export const AuthContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AuthReducer, initialState);
return (
<AuthContext.Provider value={{
userID: state.userID,
isAuthenticated: state.isAuthenticated,
error: state.error,
dispatch,
}}>
{children}
</AuthContext.Provider>
)
}
AuthActions.js:
export const LoginStart = (userCredentials) => ({
type: "LOGIN_START",
});
export const LoginSuccess = (userID) => ({
type: "LOGIN_SUCCESS",
payload: userID,
isAuthenticated: true,
});
export const LoginFailure = (error) => ({
type: "LOGIN_FAILURE",
payload: error,
isAuthenticated: false,
});
AuthReducer.js:
const AuthReducer = (state, action) => {
switch(action.type) {
case "LOGIN_START":
return {
userID: null,
isAuthenticated: false,
error: false,
}
case "LOGIN_SUCCESS":
return {
userID: action.payload,
isAuthenticated: true,
error: false,
}
case "LOGIN_FAILURE":
return {
userID: null,
isAuthenticated: false,
error: action.payload,
}
default:
return state;
}
}
export default AuthReducer;
Any help is appreciated, thanks!
I figured this out, I simply had to move the AuthContext Provider out of App.js and into index.js
I am getting the data from the api as expected but the problem here is I am getting it in a 3rd attempt which is causing the error in my application when there's no data to show.
I am testing it printing on a console but it's the same error. As Soon As I Refresh My Page The Error Comes Flooding In The Console
Reducer
export const productDetailsReducer = (state = { products: {} }, action) => {
switch (action.type) {
case PRODUCT_DETAILS_REQUEST:
return {
...state,
loading: true,
};
case PRODUCT_DETAILS_SUCCESS:
return {
loading: false,
product: action.payload,
};
case PRODUCT_DETAILS_FAIL:
return {
...state,
error: action.payload,
};
case CLEAR_ERRORS:
return {
...state,
error: null,
};
default:
return state;
}
};
Component
const ProductDetails = () => {
const dispatch = useDispatch();
const alert = useAlert();
const { id } = useParams();
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
useEffect(() => {
dispatch(getProductDetails(id));
if (error) {
alert.error(error);
dispatch(clearErrors());
}
}, [dispatch, id, alert, error]);
console.log(product);
Action
export const getProductDetails = (id) => async (dispatch) => {
try {
dispatch({ type: PRODUCT_DETAILS_REQUEST });
const { data } = await axios.get(`/api/v1/product/${id}`);
dispatch({
type: PRODUCT_DETAILS_SUCCESS,
payload: data.product,
});
} catch (error) {
dispatch({
type: PRODUCT_DETAILS_FAIL,
payload: error.response.data.message,
});
}
};
App.js
function App() {
return (
<Router>
<div className="App">
<Header />
<div className="container container-fluid">
<Routes>
<Route path="/" element={<Home />} exact />
<Route path="/product/:id" element={<ProductDetails />} />
</Routes>
</div>
<Footer />
</div>
</Router>
);
}
You haven't set the loading in the initial state I think that's why
export const productDetailsReducer = (state = { products: {} }, action) => {
you are trying to access the loading state before you set the value of the state that's why you are getting the error
you should set the loading and error in the initial state with the default values
const initialState = {products: {}, loading: true, error: null}
and then pass it to the reducer.
export const productDetailsReducer = (state = initialState, action) => {
and change the product into products
hopefully, this will fix your issue.
I have checked your code and I think the problem is with your initial state in
export const productDetailsReducer = (state = { products: {} }, action) =>
try changing state={products:{}} with state={product: {}}
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:
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.
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.
Good day, I faced an issue when tried to push the user to the dashboard after the user login correctly but it didn't, here is the code below:
LoginForm.js
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
e.preventDefault();
console.log(values);//values={email:'..', pass:'..'}
if (formValidation()) {
dispatch(NewUserLogin(values));
console.log(isAuth); //print false but in redux state print true
if (isAuth) history.push('/dashboard');
}
};
LoginAction.js
export const NewUserLogin = (formValues) => async (dispatch) => {
try {
dispatch(loginPending());
const { status, message } = await LoginAPIRequest(formValues);
if (status === 'success') {
dispatch(loginSuccess(message));
} else {
dispatch(loginFailure(message));
}
console.log(status);
console.log(message);
} catch (error) {
dispatch(loginFailure(error.message));
}
};
loginSlice.js
import { createSlice } from '#reduxjs/toolkit';
const initialState = {
isLoading: false,
isAuth: false,
error: '',
};
const loginSlice = createSlice({
name: 'Login',
initialState,
reducers: {
loginPending: (state) => {
state.isLoading = true;
},
loginSuccess: (state, { payload }) => {
state.isLoading = false;
state.isAuth = true;
state.message = payload;
state.error = '';
},
loginFailure: (state, { payload }) => {
//actions.payload or shortcut {payload}
state.isLoading = false;
state.error = payload;
},
},
});
const { reducer, actions } = loginSlice;
export const { loginPending, loginSuccess, loginFailure } = actions;
export default reducer;
userAPI.js
import { createEndpointsAPI, ENDPOINTS } from './index';
export const LoginAPIRequest = (formValues) => {
return new Promise(async (resolve, reject) => {
//call api
try {
await createEndpointsAPI(ENDPOINTS.LOGIN)
.create(formValues)
.then((res) => {
resolve(res.data);
if (res.data.status === 'success') {
resolve(res.data);
sessionStorage.setItem('accessJWT', res.data.accessJWT);
localStorage.setItem('sms', JSON.stringify(res.data.refreshJWT));
}
console.log(res.data);
})
.catch((err) => {
reject(err);
});
} catch (error) {
console.log(error);
reject(error);
}
});
};
index.js (root API)
import axios from 'axios';
export const ENDPOINTS = {
LOGIN: 'user/login',
LOGOUT: 'user/logout',
REGISTER: 'user/register',
};
const baseURL = 'http://localhost:3040/v2/';
export const createEndpointsAPI = (endpoint) => {
let url = baseURL + endpoint + '/';
return {
fetchAll: () => axios.get(url),
fetchById: (id) => axios.get(url + id),
create: (newData) => axios.post(url, newData),
update: (updateData, id) => axios.put(url + id, updateData),
delete: (id) => axios.delete(url + id),
};
};
App.js
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Switch>
<Route path='/' exact>
<Login />
</Route>
<PrivateRoute path='/dashboard'>
<Dashboard />
</PrivateRoute>
<Route path='*' component={() => '404 NOT FOUND'} />
</Switch>
</Router>
</MuiThemeProvider>
PrivateRoute.js
import { useSelector } from 'react-redux';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuth } = useSelector((state) => state.login);
console.log(isAuth);
return (
<Route
{...rest}
render={(props) => {
isAuth ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/',
state: { from: props.location },
}}
/>
);
}}
/>
);
};
export default PrivateRoute;
The problem is, isAuth is a redux state, it should return true when the user login correctly, but it's not, I console.log(isAuth) and it prints false for the first time even user login correctly, and if I click login one more time it prints true in the console log and redirects the user to the dashboard page. I don't know why isAuth is returned false for the first time when use is login correctly? Please help check the above code from top to bottom, I provide you everythings.
The log: console.log(isAuth); logs a stale closure, you could try an effect on isAuth and redirect when it's true.
Here is an example:
const Component = (propps) => {
const { isLoading, isAuth, error, message } = useSelector(
(state) => state.login
);
const handleSubmit = (e) => {
//...dispatches but doesn't check isAuth
};
useEffect(() => {
//go to dashboard if isAuth is true
if (isAuth) history.push('/dashboard');
}, [isAuth]);//run effect when isAuth changes
};
I was working on an old app using react navigation version 4 the app contains a register and login in page obviously and then the content of the app.
recently I started remaking the content of the app using react navigation version 5 in order to use the shared element animation and the bottom tab navigator and it was fairly simple.
but I struggled with converting the login part to version 5 since the app structure is somewhat complicated and I am somewhat new to react navigation version 5.
i will leave a figure of the app structure bellow a long with samples of the code used.
App.js :
import { setNavigator } from "./app/navigationRef";
const articleListFlow = createStackNavigator({
Main: MainScreen, // screen with diffrent articles categories
ResultsShow: ResultShowScreen, // article details screen
});
const loginFlow = createStackNavigator({
Signup: SignupScreen,
Signin: SigninScreen,
});
loginFlow.navigationOptions = () => {
return {
headerShown: false,
};
};
articleListFlow.navigationOptions = {
title: "News Feed",
tabBarIcon: ({ tintColor }) => (
<View>
<Icon style={[{ color: tintColor }]} size={25} name={"ios-cart"} />
</View>
),
activeColor: "#ffffff",
inactiveColor: "#ebaabd",
barStyle: { backgroundColor: "#d13560" },
};
const switchNavigator = createSwitchNavigator({
ResolveAuth: ResolveAuthScreen,
MainloginFlow: createSwitchNavigator({
//WelcomeScreen: WeclomeScreen,
loginFlow: loginFlow,
}),
mainFlow: createMaterialBottomTabNavigator(
{
articleListFlow: articleListFlow,
ArticleSave: ArticleSaveScreen, // we dont need this one
Account: AccountScreen,
},
{
activeColor: "#ffffff",
inactiveColor: "#bda1f7",
barStyle: { backgroundColor: "#6948f4" },
}
),
});
const App = createAppContainer(switchNavigator);
export default () => {
return (
<AuthProvider>
<App
ref={(navigator) => {
setNavigator(navigator);
}}
/>
</AuthProvider>
);
};
NavigationRef.js :
import { NavigationActions } from "react-navigation";
let navigator;
export const setNavigator = (nav) => {
navigator = nav;
};
export const navigate = (routeName, params) => {
navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
};
// routename is the name of the routes singin singup accountscreen
// params information we want to pass to the screen we want to show
AuthContext.js
import { AsyncStorage } from "react-native";
import createDataContext from "./createDataContext";
import userAPI from "../api/user";
// using navigate to access the navigator and redirect the user
import { navigate } from "../navigationRef";
// AUTHENTICATION REDUCERS
const authReducer = (state, action) => {
switch (action.type) {
case "add_error": {
return {
...state,
errorMessage: action.payload,
};
}
case "clear_error_message": {
return {
...state,
errorMessage: "",
};
}
case "signin": {
return {
errorMessage: "",
token: action.payload,
};
}
default:
return state;
}
};
// CLEARING ERROR MESSAGES WHEN SWITCHING SIGNIN-SIGNUP
const clearErrorMessage = (dispatch) => () => {
dispatch({ type: "clear_error_message" });
};
// AUTOMATIC SIGNIN ONLY USING TOKENS ON USER DEVICE
const tryLocalSignin = (dispatch) => async () => {
const token = await AsyncStorage.getItem("token");
if (token) {
// if token exists
dispatch({ type: "signin", payload: token });
navigate("Main");
} else {
// if token doesnt exist
navigate("WelcomeScreen");
}
};
// SIGNUP
const signup = (dispatch) => async ({ email, password }) => {
try {
const response = await userAPI.post("/signup", { email, password });
await AsyncStorage.setItem("token", response.data.token);
dispatch({ type: "signin", payload: response.data.token });
// making use of the navigate component to access navigation
// and redirect the user
navigate("Main");
} catch (err) {
dispatch({
type: "add_error",
payload: "Something went wrong with sign up",
});
}
};
// SIGNIN
const signin = (dispatch) => async ({ email, password }) => {
try {
const response = await userAPI.post("/signin", { email, password });
await AsyncStorage.setItem("token", response.data.token);
// using signin since the logic is the same
dispatch({ type: "signin", payload: response.data.token });
// making use of the navigate component to access navigation
// and redirect the user
navigate("Main");
} catch (err) {
console.log(err);
dispatch({
type: "add_error",
payload: "Something went wrong with sign in",
});
}
};
// SIGNOUT
const signout = (dispatch) => async () => {
// removing the token makes identification not work again
await AsyncStorage.removeItem("token");
dispatch({ type: "signout" });
navigate("loginFlow");
};
// CREATING CONTEXT AND PROVIDER OBJECTS FOR AUTHENTICATION
export const { Provider, Context } = createDataContext(
authReducer,
{
signin,
signup,
signout,
clearErrorMessage,
tryLocalSignin,
},
{
token: null,
errorMessage: "",
}
);
createDataContext.js
import React, { useReducer } from "react";
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let action in actions) {
// for every action in the actions, call it with dispatch
boundActions[action] = actions[action](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
My appologies for the long code and thank you in advance for anyone who can help.
There are several things that you need to consider when moving from V4 to V5 it involves some changes and also you can consider using features like the hooks.
The first change will be removing the Switch Navigator and conditionally render the navigator in its place. This will be done in your App.js. As you already have a reducer based implementation you can use the state values to take this decision.
The next change will be the creation of stacks, in V4 you create the navigation by passing the screen, now everything is a component and you pass the screens as children.
The option are also sent as props to either the navigator or the screen itself.
The usage of navigation ref is still possible but you can also use hooks like usenavigation inside components and for your authentication flow you wont be using this as you conditionally render the navigators.
I have made a simplified version based on your code.
App.js
const AuthStack = createStackNavigator();
const AppTabs = createMaterialBottomTabNavigator();
const ArticleStack = createStackNavigator();
const Articles = () => {
return (
<ArticleStack.Navigator>
<AppTabs.Screen name="ArticlesList" component={ArticleList} />
<AppTabs.Screen name="ArticlesDetails" component={ArticleDetail} />
</ArticleStack.Navigator>
);
};
export default function App() {
const [state, dispatch] = React.useReducer(authReducer, {
isLoading: true,
token: null,
errorMessage: '',
});
React.useEffect(() => {
const bootstrapAsync = async () => {
const userToken = await AsyncStorage.getItem('userToken');
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
{state.token === null ? (
<AuthStack.Navigator headerMode="none">
{state.isLoading ? (
<AuthStack.Screen name="Welcome" component={WelcomeScreen} />
) : (
<>
<AuthStack.Screen name="SignIn" component={SignInScreen} />
<AuthStack.Screen name="SignUp" component={SingUpScreen} />
</>
)}
</AuthStack.Navigator>
) : (
<AppTabs.Navigator
activeColor="#f0edf6"
inactiveColor="#3e2465"
barStyle={{ backgroundColor: '#694fad' }}>
<AppTabs.Screen
name="Articles"
component={Articles}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => (
<MaterialCommunityIcons
name="home"
color={color}
size={size}
/>
),
}}
/>
<AppTabs.Screen name="Search" component={SearchScreen} />
<AppTabs.Screen name="Save" component={SaveScreen} />
<AppTabs.Screen name="Account" component={AccountScreen} />
</AppTabs.Navigator>
)}
</NavigationContainer>
</AuthContext.Provider>
);
}
Auth Context
const AuthContext = React.createContext();
export default AuthContext;
Auth Reducer
export const authReducer = (state, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...state,
token: action.token,
isLoading: false,
};
case 'SIGN_IN': {
return {
errorMessage: '',
token: action.payload,
};
}
case 'SIGN_OUT': {
return {
errorMessage: '',
token: null,
};
}
default:
return state;
}
};
As you can see the flow will be showing the welcome screen till the token is loaded from async storage and then based on that show the tabs or the login screen. Also the parameters are passed as props. I've moved the actions to app.js but it can be separated as well.
You can see a fully running sample here
https://snack.expo.io/#guruparan/navigation-sample-3
Hope this helps, Feel free to ask if there are any questions.
As per your diagram, I have tried to create Navigation
const WelcomeStack = createStackNavigator();
const Tab = createBottomTabNavigator();
const ArticleStack = createStackNavigator();
const MainStack = createStackNavigator();
function Welcome(){
return(
<WelcomeStack.Navigator>
<WelcomeStack.screen name='SignIn' component={SignIn}/>
<WelcomeStack.screen name='SignUp' component={SignUp}/>
</WelcomeStack.Navigator>
)
}
function Article(){
return(
<ArticleStack.Navigator>
<ArticleStack.Screen name='ArtcileList' name={ArticleList}/>
<ArticleStack.Screen name='ArticleDetail' name={ArtcileDetail}/>
</ArticleStack.Navigator>
)
}
function TabNav(){
<Tab.Navigator>
<Tab.Screen name='Article' component={Article}/>
<Tab.Screen name='Search' component={Search}/>
<Tab.Screen name='Save' component={Save}/>
<Tab.Screen name='Account' component={Account}/>
</Tab.Navigator>
}
function App(){
return(
<NavigationContainer>
<MainStack.Navigator>
{this.state.isLogin ?
<MainStack.Screen name='Tab' component={TabNav}/>
:
<MainStack.Screen name = 'WelcomeStack' component={Welcome}/>
}
</MainStack.Navigator>
</NavigationContainer>
)
}
In react navigation 5, their is no switch navigator so you have to go with stack navigation + ternary operator.
This is just an idea as per your diagram. You can make it better after some R&D.