Related
I am making an API call using Axios and after that I am send those details to context API but I am getting null. I am using formik to send data to backend and on submit of that form I make an api call using axios then get the user from backend end pass it on to context API.
UserContext
import { createContext, useReducer } from "react";
import UserReducer from "./UserReducer";
const INITIAL_STATE = {
user: null,
};
export const UserContext = createContext(INITIAL_STATE);
export const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(UserReducer, INITIAL_STATE);
const setUser = (userDetails) => {
dispatch({
type: "GET_USER",
payload: userDetails,
});
// Here it is returning the user data but INITIAL_STATE.user is null
};
return (
<UserContext.Provider
value={{
user: state.user,
setUser,
}}
>
{children}
</UserContext.Provider>
);
};
UserReducer
const UserReducer = (state, action) => {
switch (action.type) {
case "GET_USER":
return {
user: action.payload,
};
default:
return state;
}
};
export default UserReducer;
Login
const { user, setUser } = useContext(UserContext);
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: () => {
const getUser = async () => {
const userData = await Axios.post("http://localhost:3001/login", {
email: formik.values.email,
password: formik.values.password,
});
setUser(userData.data); // Here I am sending the data to context API
};
getUser();
},
validationSchema,
});
I'm trying to make protected routes for my website using React and firebase. It works fine when users log-in with email but when users log-in with firebase, the page refreshes before it has time to update verification status so it reloads to the log-in page again but when I manually refresh, it goes to the protected page. I'm not sure how to fix so that it will work for firebase log-in as well.
Actions.js:
import * as types from "./actionTypes";
import { auth, googleAuthProvider, facebookAuthProvider } from '../Firebase';
const registerStart = () => ({
type: types.REGISTER_START,
});
const registerSuccess = (user) => ({
type: types.REGISTER_SUCCESS,
payload: user,
});
const registerFail = (error) => ({
type: types.REGISTER_FAIL,
payload: error,
});
const loginStart = () => ({
type: types.LOGIN_START,
});
const loginSuccess = (user) => ({
type: types.LOGIN_SUCCESS,
payload: user,
});
const loginFail = (error) => ({
type: types.LOGIN_FAIL,
payload: error,
});
const logoutStart = () => ({
type: types.LOGOUT_START,
});
const logoutSuccess = (user) => ({
type: types.LOGOUT_SUCCESS,
});
const logoutFail = (error) => ({
type: types.LOGOUT_FAIL,
payload: error,
});
export const setUser = (user) => ({
type: types.SET_USER,
payload: user,
})
const googleSignInStart = () => ({
type: types.GOOGLE_SIGN_IN_START,
});
const googleSignInSuccess = (user) => ({
type: types.GOOGLE_SIGN_IN_SUCCESS,
});
const googleSignInFail = (error) => ({
type: types.GOOGLE_SIGN_IN_FAIL,
payload: error,
});
const fbSignInStart = () => ({
type: types.FACEBOOK_SIGN_IN_START,
});
const fbSignInSuccess = (user) => ({
type: types.FACEBOOK_SIGN_IN_SUCCESS,
});
const fbSignInFail = (error) => ({
type: types.FACEBOOK_SIGN_IN_FAIL,
payload: error,
});
export const registerInitiate = (email, password, displayName) => {
return function (dispatch) {
dispatch(registerStart());
auth.createUserWithEmailAndPassword(email, password).then(({user}) => {
user.updateProfile({
displayName
})
dispatch(registerSuccess(user));
}).catch((error) => dispatch(registerFail(error.message)))
}
};
export const loginInitiate = (email, password) => {
return function (dispatch) {
dispatch(loginStart());
auth.signInWithEmailAndPassword(email, password).then(({user}) => {
dispatch(loginSuccess(user));
}).catch((error) => dispatch(loginFail(error.message)))
}
};
export const logoutInitiate = () => {
return function (dispatch) {
dispatch(logoutStart());
auth.signOut().then((resp) =>
dispatch(logoutSuccess())
).catch((error) => dispatch(logoutFail(error.message)));
}
};
export const googleSignInInitiate = () => {
return function (dispatch) {
dispatch(googleSignInStart());
auth.signInWithPopup(googleAuthProvider).then(({user}) => {
dispatch(googleSignInSuccess(user));
}).catch((error) => dispatch(googleSignInFail(error.message)));
}
};
export const fbSignInInitiate = () => {
return function (dispatch) {
dispatch(fbSignInStart());
auth.signInWithPopup(facebookAuthProvider.addScope("user_birthday, email")).then(({user}) => {
dispatch(fbSignInSuccess(user));
}).catch((error) => dispatch(fbSignInFail(error.message)));
}
};
actionTypes.js:
export const REGISTER_START = "REGISTER_START";
export const REGISTER_SUCCESS = "REGISTER_SUCCESS";
export const REGISTER_FAIL = "REGISTER_FAIL";
export const LOGIN_START = "LOGIN_START";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAIL = "LOGIN_FAIL";
export const LOGOUT_START = "LOGOUT_START";
export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS";
export const LOGOUT_FAIL = "LOGOUT_FAIL";
export const SET_USER = "SET_USER";
export const GOOGLE_SIGN_IN_START = "GOOGLE_SIGN_IN_START";
export const GOOGLE_SIGN_IN_SUCCESS = "GOOGLE_SIGN_IN_SUCCESS";
export const GOOGLE_SIGN_IN_FAIL = "GOOGLE_SIGN_IN_FAIL";
export const FACEBOOK_SIGN_IN_START = "FACEBOOK_SIGN_IN_START";
export const FACEBOOK_SIGN_IN_SUCCESS = "FACEBOOK_SIGN_IN_SUCCESS";
export const FACEBOOK_SIGN_IN_FAIL = "FACEBOOK_SIGN_IN_FAIL";
reducer.js:
const initialState = {
loading: false,
currentUser: null,
error: null,
}
const userReducer = (state = initialState, action) => {
switch(action.type) {
case types.REGISTER_START:
case types.LOGIN_START:
case types.LOGOUT_START:
case types.GOOGLE_SIGN_IN_START:
case types.FACEBOOK_SIGN_IN_START:
return {
...state,
loading: true
};
case types.LOGOUT_SUCCESS:
return {
...state,
currentUser: null,
}
case types.SET_USER:
return {
...state,
loading: false,
currentUser: action.payload,
}
case types.REGISTER_SUCCESS:
case types.LOGIN_SUCCESS:
case types.GOOGLE_SIGN_IN_SUCCESS:
case types.FACEBOOK_SIGN_IN_SUCCESS:
return {
...state,
loading: false,
currentUser: action.payload,
};
case types.REGISTER_FAIL:
case types.LOGIN_FAIL:
case types.LOGOUT_FAIL:
case types.GOOGLE_SIGN_IN_FAIL:
case types.FACEBOOK_SIGN_IN_FAIL:
return {
...state,
loading: false,
error: action.payload,
}
default:
return state;
}
}
export default userReducer;
rootReducer.js:
import userReducer from "./reducer";
const rootReducer = combineReducers({
user: userReducer
})
export default rootReducer;
store.js:
import { createStore, applyMiddleware } from "redux";
import { createLogger } from "redux-logger";
import thunk from "redux-thunk";
import rootReducer from './rootReducer';
const middleware = [thunk];
const logger = createLogger({});
if(process.env.NODE_ENV === "development") {
middleware.push(logger)
}
export const store = createStore(rootReducer, applyMiddleware(...middleware));
login.js:
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector} from "react-redux";
import { useNavigate, Link } from "react-router-dom";
import { fbSignInInitiate, googleSignInInitiate, loginInitiate } from '../../userauth/actions';
import './login.css';
const Login = () => {
const [state, setState] = useState({
email: "",
password: "",
});
const { email, password } = state;
const { currentUser } = useSelector((state) => state.user);
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
useEffect(() => {
if(currentUser || loading) {
navigate("/dashboard");
}
}, [currentUser, navigate]);
const dispatch = useDispatch();
const handleGoogleSignIn = () => {
dispatch(googleSignInInitiate());
setLoading(true);
};
const handleFBSignIn = () => {
dispatch(fbSignInInitiate());
setLoading(true);
};
const handleSubmit = (e) => {
e.preventDefault();
if(!email || !password) {
return;
}
dispatch(loginInitiate(email, password));
setState({ email: "", password: "" });
setLoading(true);
};
const handleChange = (e) => {
let { name, value } = e.target;
setState({...state, [name]: value });
};
return (
<div>
<div id="logreg-form">
<form className='form-signin' onSubmit={handleSubmit}>
<h1>
Sign in
</h1>
<div className="social-login">
<button
className='btn google-btn social-btn'
type='button'
onClick={handleGoogleSignIn}>
<span>
<i className='fab fa-google-plus-g'>Sign in with Google</i>
</span>
</button>
<button
className='btn facebook-btn social-btn'
type='button'
onClick={handleFBSignIn}>
<span>
<i className='fab fa-facebook-f'>Sign in with Facebook</i>
</span>
</button>
</div>
<p>OR</p>
<input
type="email"
id="inputEmail"
className='form-control'
placeholder='Email Address'
name="email"
onChange={handleChange}
value={email}
required
/>
<input
type="password"
id="inputPassword"
className='form-control'
placeholder='Password'
name="password"
onChange={handleChange}
value={password}
required
/>
<button
className='btn btn-secondary btn-block'
type="submit">
<i className="fas fa-sign-in-alt"></i>Sign In
</button>
<hr />
<p>Don't have an account</p>
<Link to="/signup">
<button
className='btn btn-primary btn-block'
type="button" id="btn-signup">
<i className='fas fa-user-plus'></i>Sign up New Account
</button>
</Link>
</form>
</div>
</div>
)
}
export default Login;
Dashboard.js:
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { logoutInitiate } from '../../userauth/actions';
const Dashboard = () => {
const { currentUser } = useSelector((state) => state.user);
const dispatch = useDispatch();
const handleAuth = () => {
if(currentUser) {
dispatch(logoutInitiate());
}
}
return (
<div>
<h1>User Dashboard</h1>
<br />
<button className='btn btn-danger' onClick={handleAuth}>Logout</button>
</div>
)
}
export default Dashboard
protectedRoute.js:
import React from 'react';
import { useSelector } from 'react-redux';
import LoadingToRedirect from './LoadingToRedirect';
const ProtectedRoute = ({children}) => {
const { currentUser } = useSelector((state) => ({...state.user}));
return currentUser ? children : <LoadingToRedirect />;
}
export default ProtectedRoute;
App.js:
import React, { useEffect, useState } from 'react';
import { Landing, Login, Signup, Contact, Dashboard, Error } from './pages';
import ProtectedRoute from './components/ProtectedRoute';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { useDispatch } from "react-redux";
import { auth } from "./Firebase";
import { setUser } from './userauth/actions';
import './App.css';
const App = () => {
const dispatch = useDispatch();
useEffect(() => {
auth.onAuthStateChanged((authUser) => {
if(authUser) {
dispatch(setUser(authUser));
} else {
dispatch(setUser(null));
}
})
}, [dispatch]);
return (
<BrowserRouter>
<div className='App'>
<Routes>
<Route exact path="/" element={<Landing/>} />
<Route path="/login" element={<Login/>} />
<Route path="/signup" element={<Signup/>} />
<Route path="/contact" element={<Contact/>} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route path="*" element={<Error />} />
</Routes>
</div>
</BrowserRouter>
)
}
export default App;
If anyone comes across the same situation, I was able to solve this problem on my own and it's a simple fix. The problem was that the user authorization status wasn't updated whenever I called on Protected Routes but instead it just used the initial state when App.js was called. To get the user authorization status, I saved the user auth in my localStorage. Then, whenever I called user auth status in my redirects, I just fetched from my local storage.
I am writing a unit test for Next.js application.
The components side of the 'pages' folder return 'NextPage', in typescript, where
NextPage is imported from 'next'.
For example: import { NextPage } from 'next'; const BenchmarksPage: NextPage = () => {...
There are other components inside the 'pages' folder.
These components do not return 'NextPage', but return 'FC', which is imported from 'react'.
For example: import { FC } from 'react'; const RegisterJWT: FC = (props) => {...
In my Jest/Testing Library, I render these components like this:
render(<Component />)
expect(...)
Is this the correct way to render both the NextPage component and FC component?
Only FC components are rendering, but not NextPage components.
When rendering NextPage components, it returns <body></div></body>, an empty DOM tree.
EDIT:
Here is a NextPage component, that does not render in test:
import {
Box,
capitalize,
Container,
FormControl,
InputLabel,
MenuItem,
Select,
} from '#material-ui/core';
import { NextPage } from 'next';
import React, { useCallback, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import RequireScope from 'src/components/authentication/RequireScope';
import BenchmarkTable from 'src/components/benchmark/BenchmarkTable';
import DashboardLayout from 'src/components/dashboard/DashboardLayout';
import Heading from 'src/components/Heading';
import useSettings from 'src/hooks/useSettings';
import gtm from 'src/lib/gtm';
import { useDispatch, useSelector } from 'src/store';
import { getAllRollingTwelveCalcs } from 'src/store/rolling-twelve/rolling-twelve.thunk';
import { Timeframe, timeframeMap } from 'src/types/benchmark';
const BenchmarksPage: NextPage = () => {
const { settings } = useSettings();
const dispatch = useDispatch();
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(
Timeframe.Monthly,
);
const company = useSelector((state) => state.company.current);
useEffect(() => {
gtm.push({ event: 'page_view' });
}, []);
useEffect(() => {
dispatch(getAllRollingTwelveCalcs());
}, [company]);
const handleTimeframeChange = useCallback(
(
event: React.ChangeEvent<{
name?: string;
value: Timeframe;
event: Event | React.SyntheticEvent<Element, Event>;
}>,
) => {
setSelectedTimeframe(event.target.value);
},
[],
);
return (
<RequireScope scopes={['query:benchmark-calcs']}>
<DashboardLayout>
<Helmet>
<title>Benchmarks</title>
</Helmet>
<Container maxWidth={settings.compact ? 'xl' : false}>
<Box
sx={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
mb: 4,
}}
>
<Heading>Benchmarks</Heading>
<FormControl sx={{ width: 300 }}>
<InputLabel>Timeframe</InputLabel>
<Select
sx={{ background: '#ffffff', maxWidth: 400 }}
value={selectedTimeframe}
label="Timeframe"
onChange={handleTimeframeChange}
>
{[...timeframeMap.keys()].map((timeframe) => (
<MenuItem key={timeframe} value={timeframe}>
{capitalize(timeframe)}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<BenchmarkTable
timeframe={selectedTimeframe}
data-testid="benchmark-table"
/>
</Container>
</DashboardLayout>
</RequireScope>
);
};
export default BenchmarksPage;
It uses thunk, which is here:
import { createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'src/lib/axios';
import { RootState } from 'src/store';
import {
RollingTwelveAnalysisCategoryCalculation,
RollingTwelveManualInputLineItemCalculation,
} from 'src/types/rolling-twelve';
export const getAllRollingTwelveCalcs = createAsyncThunk<
{
anaylsisCategoryCalcs: RollingTwelveAnalysisCategoryCalculation[];
manualInputCalcs: RollingTwelveManualInputLineItemCalculation[];
},
undefined,
{
state: RootState;
}
>('rollingTwelve/getAll', async () => {
const response = await axios.get(`/rolling-twelve/all`);
return response.data;
});
Now here is a component, that is 'FC' (not NextPage) that does render. This still is inside pages folder:
import { useAuth0 } from '#auth0/auth0-react';
import {
Box,
Button,
Card,
CardContent,
Container,
Typography,
} from '#material-ui/core';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import Logo from 'src/components/Logo';
import axios from 'src/lib/axios';
import gtm from 'src/lib/gtm';
const Login: FC = () => {
const router = useRouter();
const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0();
useEffect(() => {
gtm.push({ event: 'page_view' });
}, []);
useEffect(() => {
if (!isLoading && isAuthenticated) {
router.push('/recommendations');
}
}, [isLoading, isAuthenticated]);
const handleIntuitLogin = async () => {
try {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
const handleAuth0Login = async () => {
try {
const response = await axios.get('/auth/sign-in-with-auth0');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
return (
<>
<Helmet>
<title>Login</title>
</Helmet>
<Box
sx={{
backgroundColor: 'background.default',
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
}}
>
<Container maxWidth="sm" sx={{ py: '80px' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 8,
}}
>
<Link href="/login">
<Box>
<Logo height={100} width={300} />
</Box>
</Link>
</Box>
<Card>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
p: 4,
}}
>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
mb: 3,
}}
>
<div>
<Typography
color="textPrimary"
gutterBottom
variant="h4"
data-testid="login-title"
>
Log in
</Typography>
</div>
</Box>
<Box
sx={{
flexGrow: 1,
mt: 3,
}}
>
<Button
color="primary"
onClick={handleAuth0Login}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In
</Button>
</Box>
<Box sx={{ mt: 2 }}>
<Button
color="primary"
onClick={handleIntuitLogin}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In With Intuit
</Button>
</Box>
</CardContent>
</Card>
</Container>
</Box>
</>
);
};
export default Login;
EDIT 2:
RequireScope:
import React, { useEffect, useState } from 'react';
import useAuth from 'src/hooks/useAuth';
export interface RequireScopeProps {
scopes: string[];
}
const RequireScope: React.FC<RequireScopeProps> = React.memo((props) => {
const { children, scopes } = props;
const { isInitialized, isAuthenticated, permissions } = useAuth();
const [isPermitted, setIsPermitted] = useState(false);
useEffect(() => {
if (isAuthenticated && isInitialized) {
(async () => {
const hasPermissions = scopes
.map((s) => {
return permissions.includes(s);
})
.filter(Boolean);
if (hasPermissions.length === scopes.length) {
setIsPermitted(true);
}
})();
}
}, [isAuthenticated, isInitialized, scopes, permissions]);
if (isPermitted) {
return <>{children}</>;
}
return null;
});
export default RequireScope;
Benchmarks.test.tsx
import '#testing-library/jest-dom';
import { render, screen, waitFor, within } from '#testing-library/react';
import { HelmetProvider } from 'react-helmet-async';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import BenchmarksPage from '../src/pages/benchmarks/index';
import { initState } from './mockState'; // this is just an object with nested objects and arrays
test('renders header, timeframe dropdown and the chart image', async () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
render(
<HelmetProvider>
<Provider store={mockStore(initState)}>
<BenchmarksPage />
</Provider>
</HelmetProvider>,
);
// Note: NONE of these 5 queries below work. Component is not rendering in the DOM
const header = screen.getByRole('heading', { name: /benchmarks/i });
const header = screen.getByRole('heading');
const header = await screen.findByRole('heading', { name: /benchmarks/i });
const header = await waitFor(() => screen.getByText(/benchmarks/i));
const header = await waitFor(() => expect(screen.getByText(/benchmarks/i)), {
timeout: 4000,
});
screen.debug(); // doesn't work. <body></div></body>
}
EDIT3:
useAuth in RequireScope
import { useContext } from 'react';
import AuthContext from '../contexts/JWTContext';
const useAuth = () => useContext(AuthContext);
export default useAuth;
JWTContext above:
Just really long, dispatches to /login using JWT token etc.
EDIT 4:
JWTContext component
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';
import {
createContext,
FC,
ReactNode,
useCallback,
useEffect,
useReducer,
} from 'react';
import { useDispatch } from 'src/store';
import { companyActions } from 'src/store/company/company.slice';
import {
getAllCompanies,
getCurrentCompany,
} from 'src/store/company/company.thunk';
import axios from '../lib/axios';
export interface AuthUser {
sub?: string;
nickname?: string;
email?: string;
name?: string;
picture?: string;
locale?: string;
updated_at?: Date;
}
interface State {
isInitialized: boolean;
isAuthenticated: boolean;
permissions: string[];
user: AuthUser;
}
interface AuthContextValue extends State {
platform: 'JWT';
login: () => Promise<void>;
logout: () => Promise<void>;
}
interface AuthProviderProps {
children: ReactNode;
}
type InitializeAction = {
type: 'INITIALIZE';
payload: {
isAuthenticated: boolean;
permissions: string[];
user: AuthUser;
};
};
type LoginAction = {
type: 'LOGIN';
};
type LogoutAction = {
type: 'LOGOUT';
};
type Action = InitializeAction | LoginAction | LogoutAction;
const initialState: State = {
isAuthenticated: false,
isInitialized: false,
permissions: [],
user: undefined,
};
const setSession = (
accessToken?: string,
permissions?: string,
user?: string,
): void => {
if (accessToken && permissions) {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('perms', permissions);
localStorage.setItem('user', user);
//axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
} else {
localStorage.removeItem('accessToken');
localStorage.removeItem('perms');
localStorage.removeItem('user');
//delete axios.defaults.headers.common.Authorization;
}
};
const handlers: Record<string, (state: State, action: Action) => State> = {
INITIALIZE: (state: State, action: InitializeAction): State => {
const { isAuthenticated, permissions, user } = action.payload;
return {
...state,
isAuthenticated,
isInitialized: true,
permissions,
user,
};
},
LOGIN: (state: State): State => {
return {
...state,
isAuthenticated: true,
};
},
LOGOUT: (state: State): State => ({
...state,
isAuthenticated: false,
permissions: [],
}),
};
const reducer = (state: State, action: Action): State =>
handlers[action.type] ? handlers[action.type](state, action) : state;
const AuthContext = createContext<AuthContextValue>({
...initialState,
platform: 'JWT',
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
});
export const AuthProvider: FC<AuthProviderProps> = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const reduxDispatch = useDispatch();
useEffect(() => {
const initialize = async (): Promise<void> => {
try {
if (router.isReady) {
const { token, permissions, user, companyId } = router.query;
// TODO: Move all of this stuff from query and localstorage into session
const accessToken =
(token as string) || window.localStorage.getItem('accessToken');
const permsStorage = window.localStorage.getItem('perms');
const perms = (permissions as string) || permsStorage;
const userStorage = window.localStorage.getItem('user');
const selectedCompanyId =
(companyId as string) || window.localStorage.getItem('companyId');
const authUser = (user as string) || userStorage;
if (accessToken && perms) {
setSession(accessToken, perms, authUser);
try {
// check if user is admin by this perm, probably want to add a flag later
if (perms.includes('create:calcs')) {
if (!selectedCompanyId) {
const response = await reduxDispatch(getAllCompanies());
const companyId = response.payload[0].id;
reduxDispatch(companyActions.selectCompany(companyId));
reduxDispatch(getCurrentCompany({ companyId }));
} else {
reduxDispatch(
companyActions.selectCompany(selectedCompanyId),
);
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} else {
reduxDispatch(companyActions.selectCompany(selectedCompanyId));
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} catch (e) {
console.warn(e);
} finally {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
permissions: JSON.parse(perms),
user: JSON.parse(authUser),
},
});
}
if (token || permissions) {
router.replace(router.pathname, undefined, { shallow: true });
}
} else {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
setSession(undefined);
if (router.pathname !== '/client-landing') {
router.push('/login');
}
}
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
//router.push('/login');
}
};
initialize();
}, [router.isReady]);
const login = useCallback(async (): Promise<void> => {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
}, []);
const logout = useCallback(async (): Promise<void> => {
const token = localStorage.getItem('accessToken');
// only logout if already logged in
if (token) {
dispatch({ type: 'LOGOUT' });
}
setSession(null);
router.push('/login');
}, [dispatch, router]);
return (
<AuthContext.Provider
value={{
...state,
platform: 'JWT',
login,
logout,
}}
>
{state.isInitialized && children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthContext;
When I render a component by wrapping it with AuthProvider, I get the error
TypeError: Cannot read property 'isReady' of null
217 |
218 | initialize();
> 219 | }, [router.isReady]);
I searched the whole day how to make the router not to be null, but I could not.
If there are any other suggestions on how to simulate a logged in user given the code above, I would greatly appreciate it. THank you.
What I am tying to do is when the user clicks on sign in button my action gets dispatch with email and password.
But, my action is not getting dispatched. Like when I checked my redux-dev-tools it is not showing anything:
There are no error message in console. I checked other answer's but nothing helped.
Here is the source code:
LoginScreen.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import ErrorMessage from "../../components/ErrorMessage/ErrorMessage";
import Loader from "../../components/Loader/Loader";
import { login } from "../../redux/actions/userActions";
import "./LoginScreen.scss";
const LoginScreen = ({ location, history }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const dispatch = useDispatch();
const userLogin = useSelector((state) => state.userLogin);
const { loading, error, userInfo } = userLogin;
const redirect = location.search ? location.search.split("=")[1] : "/";
useEffect(() => {
if (userInfo) {
history.push(redirect);
}
}, [history, userInfo, redirect]);
const submitHandler = (e) => {
e.preventDefault();
dispatch(login(email, password));
};
return (
<>
<div className="login-container">
<div className="login-form">
<h1>Login</h1>
{loading ? (
<Loader />
) : error ? (
<ErrorMessage error={error} />
) : (
<form onSubmit={submitHandler}>
<div className="login-form-items">
<input
className="login-input"
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
className="login-input"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit" value="submit">
Login
</button>
<h4>OR</h4>
<div className="login-form-social">
<button className="social">
<img
className="googleLogo"
src="/logo/google.svg"
alt="G"
/>{" "}
Login with Google
</button>
<button className="social social-github">
<img
className="githubLogo"
src="/logo/github.svg"
alt="GH"
/>{" "}
Login with GitHub
</button>
</div>
</div>
</form>
)}
</div>
</div>
</>
);
};
export default LoginScreen;
userAction.js
import axios from "axios";
import {
USER_LOGIN_FAIL,
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
} from "../constants/userConstants";
export const login = () => (email, password) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const config = {
headers: {
"Content-Type": "appllication/json",
},
};
const { data } = await axios.post(
"/api/users/login",
{ email, password },
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
userReducer.js
import {
USER_LOGIN_FAIL,
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGOUT,
} from "../constants/userConstants";
export const userLoginReducer = (state = {}, action) => {
switch (action.type) {
case USER_LOGIN_REQUEST:
return { loading: true };
case USER_LOGIN_SUCCESS:
return { loading: false, userInfo: action.payload };
case USER_LOGIN_FAIL:
return { loading: false, error: action.payload };
case USER_LOGOUT:
return {};
default:
return state;
}
};
store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
// reducers
import { userLoginReducer } from "./reducers/userReducers";
const reducer = combineReducers({
userLogin: userLoginReducer,
});
const userInfoFromStorage = localStorage.getItem("userInfo")
? JSON.parse(localStorage.getItem("userInfo"))
: null;
const initialState = {
userLogin: { userInfo: userInfoFromStorage },
};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
You've defined your action wrong. With redux-thunk you define your actions like this:
export const login = (email, password) => async (dispatch) => {
// your action code
};
// The above code is equivalent to
export const login = (email, password) => {
return async (dispatch) => {
// your action code
}
}
Not like this:
export const login = () => (email, password) => async (dispatch) => {
// your action code
};
// The above code is equivalent to
export const login = () => {
return (email, password) => {
return async (dispatch) => { // this is wrong
}
}
}
So your action is returning a function which then returns another function.
The way you use it caught my attention. Out of general use. Generally, api operations are done with packages such as saga or thunk. Action is only used as a hyperlink. I suggest you review the article below. I think this build will solve your problem.
https://blog.devgenius.io/reactjs-simple-understanding-redux-with-redux-saga-f635e273e24a
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.