I created a custom hook (useAuth) to extend the third-party Authentication service Auth0's useAuth0 hook and set some local variables that holds basic user information, such as userId.
I have a master account that can impersonate other accounts. This means that it overrides the userId from my custom hook and it gets propagated throughout the system.
The problem that I'm facing is that whenever I call the impersonate function that changes this hook's inner state, it changes it, but then reinitializes itself. I don't know what is causing this reinitialization. The code is down below.
import { useAuth0 } from '#auth0/auth0-react';
import produce from 'immer';
import { useState, useEffect, useCallback, useReducer, Reducer } from 'react';
import { AccountType, Auth0HookUser, TenantInfo, TenantType } from '../#dts';
type AuthVariants =
| 'INDIVIDUAL_TEACHER'
| 'INSTITUTION_TEACHER'
| 'STUDENT'
| 'SECRETARY'
| 'COORDINATOR'
| 'ADMINISTRATOR';
type AuthTenant = {
accountType: AccountType;
tenantType: TenantType;
employeeId: string;
tenantId: string;
selectedTenant: TenantInfo;
variant: AuthVariants;
mode: 'IMPERSONATION' | 'NORMAL';
user: Auth0HookUser;
};
const defaultAuthTenant: () => AuthTenant = () => ({
accountType: 'teacher',
employeeId: '',
mode: 'NORMAL',
selectedTenant: {
accountType: 'teacher',
tenantType: 'INSTITUTION',
tenantId: '',
},
tenantId: '',
tenantType: 'INSTITUTION',
variant: 'INDIVIDUAL_TEACHER',
user: {
name: '',
nickname: '',
} as any,
});
type Action =
| {
type: 'UPDATE_AUTH';
auth: AuthTenant;
}
| {
type: 'IMPERSONATE';
impersonatedEmployeeId: string;
impersonatedName: string;
accountType: AccountType;
}
| {
type: 'EXIT_IMPERSONATION';
};
type State = {
current: AuthTenant;
original: AuthTenant;
};
const reducer = produce((state: State, action: Action) => {
switch (action.type) {
case 'IMPERSONATE':
console.log('Impersonating');
const selectedTenant =
state.current.user['https://app.schon.io/user_data'].tenants[0];
state.current = {
...state.current,
user: {
...state.current.user,
name: action.impersonatedName,
nickname: action.impersonatedName,
'https://app.schon.io/user_data': {
...state.current.user['https://app.schon.io/user_data'],
userId: action.impersonatedEmployeeId,
},
},
mode: 'IMPERSONATION',
accountType: action.accountType,
employeeId: action.impersonatedEmployeeId,
variant: getVariant(action.accountType, selectedTenant.tenantType),
selectedTenant: {
...state.current.selectedTenant,
accountType: action.accountType,
},
};
return state;
case 'UPDATE_AUTH':
state.current = action.auth;
state.original = action.auth;
return state;
default:
return state;
}
});
export function useAuth() {
const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
const user = _user as Auth0HookUser;
const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
current: defaultAuthTenant(),
original: defaultAuthTenant(),
});
const impersonate = (
impersonatedEmployeeId: string,
accountType: AccountType,
impersonatedName: string,
) => {
if (!user) {
return;
}
dispatch({
type: 'IMPERSONATE',
accountType,
impersonatedEmployeeId,
impersonatedName,
});
};
const exitImpersonation = useCallback(() => {
dispatch({ type: 'EXIT_IMPERSONATION' });
}, []);
useEffect(() => {
if (isLoading || (!isLoading && !isAuthenticated)) {
return;
}
if (!user || state.current.mode === 'IMPERSONATION') {
return;
}
console.log('Use Effect Running');
const { tenants, userId } = user['https://app.schon.io/user_data'];
const selectedTenant = tenants[selectedTenantIndex];
const { accountType, tenantType } = selectedTenant;
dispatch({
type: 'UPDATE_AUTH',
auth: {
tenantId: selectedTenant.tenantId,
employeeId: userId,
mode: 'NORMAL',
variant: getVariant(accountType, tenantType),
user,
selectedTenant,
accountType,
tenantType,
},
});
}, [
user,
isAuthenticated,
isLoading,
selectedTenantIndex,
state.current.mode,
]);
console.log('State Current', state.current);
return {
isAuthenticated,
isLoading,
impersonate,
exitImpersonation,
setSelectedTenantIndex,
...auth,
...state.current,
};
}
function getVariant(
accountType: AccountType,
tenantType: TenantType,
): AuthVariants {
if (accountType === 'teacher') {
return tenantType === 'INSTITUTION'
? 'INSTITUTION_TEACHER'
: 'INDIVIDUAL_TEACHER';
}
return accountType.toUpperCase() as AuthVariants;
}
See the picture. After I call the impersonate function it sets it to the impersonated mode but re-initializes itself and sets it to the default.
This is what I've tried:
Double Checked that proper dependencies were passed to the useEffect (it is not the one causing the re-initialize).
I was using a useStae before the reducer, and I was calling it via its function vs setting the state directly.
I tried stepping in (debugging) throughout the entire cycle, and didn't find anything.
I went through several SO posts and React dosc to see if I could find any issues, but my blinded eye couldn't see it.
Here's a view where I'm calling it from (See the const {impersonate} = useAuth()) :
import React, { memo, useCallback, useMemo, useState } from 'react';
import { RouteComponentProps } from '#reach/router';
import { Button, Typography } from 'components';
import Skeleton from 'react-loading-skeleton';
import { useAuth } from '../../../../../auth';
import { Tabs, Dialog } from '../../../../../components/';
import { useAllClassesAndTeacherForInstitution } from '../../../../../graphql';
import { useThemeSpacing } from '../../../../../shared-styles/material-ui';
import { AddClassTeacher, ListClassTeacher } from './components';
type TeacherViewRouteProps = {
teacherId: string;
};
export const TeacherView: React.FC<RouteComponentProps<
TeacherViewRouteProps
>> = memo((props) => {
const { impersonate } = useAuth();
const { teacherId } = props;
const { data, loading } = useAllClassesAndTeacherForInstitution(teacherId!);
const [open, setOpen] = useState(false);
const openDialog = useCallback(() => setOpen(true), []);
const closeDialog = useCallback(() => setOpen(false), []);
const spacing = useThemeSpacing(4)();
const teacherName = `${data?.teacher.name.fullName}`;
const impersonateTeacher = useCallback(() => {
if (!teacherName || !teacherId) {
return;
}
impersonate(teacherId!, 'teacher', teacherName);
closeDialog();
// props?.navigate?.('/');
}, [impersonate, closeDialog, teacherId, teacherName]);
const tabOptions = useMemo(
() => [
{
label: `Clases de ${teacherName}`,
},
{
label: 'Agregar Clases',
},
],
[teacherName],
);
return (
<>
<Typography variant="h1" className={spacing.marginTopBottom}>
{(loading && <Skeleton />) || teacherName}
</Typography>
<Dialog
title={`Entrar en la cuenta de ${teacherName}`}
open={open}
onAgree={impersonateTeacher}
onClose={closeDialog}
>
¿Desea visualizar la cuenta de {teacherName}?
<br />
Si desea salir de la misma por favor refresque la página.
</Dialog>
<Button className={spacing.marginTopBottom} onClick={openDialog}>
Entrar en cuenta de {teacherName || 'maestro'}
</Button>
{process.env.NODE_ENV === 'development' && (
<>
<Tabs options={tabOptions}>
<>
{data?.teacher.klasses && (
<ListClassTeacher
klasses={data.teacher.klasses}
teacherName={teacherName || 'maestro'}
/>
)}
</>
<>
{data?.grades && (
<AddClassTeacher
existingClasses={data?.teacher.klasses || []}
grades={data.grades}
teacherId={teacherId!}
/>
)}
</>
</Tabs>
</>
)}
</>
);
});
export default TeacherView;
Here's the initial Provider:
import React, { Suspense, memo } from 'react';
import { Location } from '#reach/router';
import { ThemeProvider } from '#material-ui/core';
import { ApolloProvider } from '#apollo/client';
import { theme } from 'components';
import { Auth0Provider } from '#auth0/auth0-react';
import CircularLoader from './components/CircularProgress';
import { useGlobalClient } from './utilities/client';
import { Layout } from './views/Layout';
import { Root } from './views/Root';
import { enableIfNotPreRendering } from './utilities/isPrerendering';
import { AUTH_CONFIG } from './auth/auth0.variables';
console.log('AUTH CONFIG', AUTH_CONFIG);
function App() {
// This will be a method to enable faster loading times.
/**\
* Main AppMethod which hosts the site. To improve FCP it was split into
* 2 files: The main file which will load the <Home component without any
* dependencies (making it extremely fast to load at the beginning as it won't)
* download all the code on its entirety.
*
* All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
* The propagation from Provider to its descendant consumers is not subject to the
* shouldComponentUpdate method, so the consumer is updated even when an ancestor component
* bails out of the update.
*
* Check this out whenever you're planning on implementing offline capabilities:
* https://dev.to/willsamu/how-to-get-aws-appsync-running-with-offline-support-and-react-hooks-678
*/
return (
<Suspense fallback={<CircularLoader scrollsToTop={true} />}>
<Location>
{({ location }) => (
<Auth0Provider
{...AUTH_CONFIG}
location={{ pathname: location.pathname, hash: location.hash }}
>
<ProviderForClient />
</Auth0Provider>
)}
</Location>
</Suspense>
);
}
/**
* This is done like this because we are using the useAuth0 Hook
* and we need it to be after the Auth0Provider!!
* #param props
*/
export const ProviderForClient: React.FC = (props) => {
const globalClient = useGlobalClient();
return (
<ThemeProvider theme={theme}>
<ApolloProvider client={globalClient.current as any}>
<Layout>
<>{enableIfNotPreRendering() && <Root />}</>
</Layout>
</ApolloProvider>
</ThemeProvider>
);
};
export default memo(App);
I feel retarded. The hook was functioning normally. There's nothing wrong with the approach above (some other things can be debated). The problem was that I was not passing the hook via a context, but I was just calling the hook on each of the components. This meant that the hook was recreating the state (as it should be) per component, so whenever I updated the state, it would only be stated in one component.
That was it. I just had to rewrite the hook as a component and share it with a context.
Here's the final code (omitted some typings due to brevity)
const AuthContext = createContext<Context>({
isAuthenticated: false,
isLoading: false,
getAccessTokenSilently() {
return '' as any;
},
getAccessTokenWithPopup() {
return '' as any;
},
getIdTokenClaims() {
return '' as any;
},
loginWithPopup() {
return '' as any;
},
loginWithRedirect() {
return '' as any;
},
logout() {
return '' as any;
},
impersonate(a: string, b: AccountType, c: string) {},
exitImpersonation() {},
setSelectedTenantIndex(i: number) {},
accountType: 'teacher',
employeeId: '',
mode: 'NORMAL',
selectedTenant: {
accountType: 'teacher',
tenantType: 'INSTITUTION',
tenantId: '',
},
tenantId: '',
tenantType: 'INSTITUTION',
user: {
name: '',
nickname: '',
} as any,
variant: 'INSTITUTION_TEACHER',
});
/**
* A custom wrapper for Auth. This allows us to set impersonation
*/
export const Auth: React.FC = memo((props) => {
const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
const user = _user as Auth0HookUser;
const defaultUser = useCallback(
(user?: Auth0HookUser) => getDefaultAuthTenant(user),
[],
);
const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
current: defaultUser(_user),
original: defaultUser(_user),
});
const calledDispatch = useCallback(dispatch, [dispatch]);
const impersonate = useCallback(
(
impersonatedEmployeeId: string,
accountType: AccountType,
impersonatedName: string,
) => {
if (!user) {
return;
}
calledDispatch({
type: 'IMPERSONATE',
accountType,
impersonatedEmployeeId,
impersonatedName,
});
},
[calledDispatch, user],
);
const exitImpersonation = useCallback(() => {
dispatch({ type: 'EXIT_IMPERSONATION' });
}, []);
useEffect(() => {
if (isLoading || (!isLoading && !isAuthenticated)) {
return;
}
if (!user || state.current.mode === 'IMPERSONATION') {
return;
}
const { tenants, userId } = user['https://app.schon.io/user_data'];
const selectedTenant = tenants[selectedTenantIndex];
const { accountType, tenantType } = selectedTenant;
dispatch({
type: 'UPDATE_AUTH',
auth: {
tenantId: selectedTenant.tenantId,
employeeId: userId,
mode: 'NORMAL',
variant: getVariant(accountType, tenantType),
user,
selectedTenant,
accountType,
tenantType,
},
});
// eslint-disable-next-line
}, [
user,
isAuthenticated,
isLoading,
selectedTenantIndex,
state.current.mode,
]);
return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
impersonate,
exitImpersonation,
setSelectedTenantIndex,
...auth,
...state.current,
}}
>
{props.children}
</AuthContext.Provider>
);
});
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error(
'You need to place the AuthContext below the Auth0Context and on top of the app',
);
}
return context;
}
Now, I just placed the <Auth/> component on the top of my app.
Related
I am trying to add Authentication to my app and maintaining Auth State using React Context API.
I am calling my api using a custom hook use-http.
import { useCallback, useReducer } from 'react';
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function useHttp(requestFunction, startWithPending = false) {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = useCallback(
async requestData => {
dispatch({ type: 'SEND' });
try {
const responseData = await requestFunction(requestData);
dispatch({ type: 'SUCCESS', responseData });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
},
[requestFunction]
);
return {
sendRequest,
...httpState,
};
}
export default useHttp;
This is my Login page which calls the api and I need to navigate out of this page and also update my Auth Context.
import { useCallback, useContext } from 'react';
import { makeStyles } from '#material-ui/core';
import Container from '#material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import useHttp from '../hooks/use-http';
import { login } from '../api/api';
import AuthContext from '../store/auth-context';
import { useEffect } from 'react';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
if (status === 'pending') {
console.log('making request');
}
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
if (status === 'completed' && error) {
console.log(error);
}
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
The login api -
export const login = async ({ email, password }) => {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: email,
password: password,
},
};
const response = await axios(config);
return response.data;
};
The Auth Context -
import React, { useState } from 'react';
import { useEffect, useCallback } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
let logoutTimer;
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const tokenData = retrieveStoredToken();
let initialToken = '';
if (tokenData) {
initialToken = tokenData.token;
}
const [token, setToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const logoutHandler = useCallback(() => {
setToken(null);
removeUser();
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
}
}, []);
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setToken(token);
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
logoutTimer = setTimeout(logoutHandler, remainingTime);
};
useEffect(() => {
if (tokenData) {
console.log(tokenData.duration);
logoutTimer = setTimeout(logoutHandler, tokenData.duration);
}
}, [tokenData, logoutHandler]);
const user = {
token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;
The problem is when I call loginHandler function of my AuthContext in Login Component, the Login component re-renders and this login function goes in an infinite loop. What am I doing wrong?
I am new to React and stuck on this issue since hours now.
I think I know what it is.
You're bringing in a bunch of component state via hooks. Whenever authCtx, sendRequest, status, data and error change, the component re-renders. Avoid putting closures into the state. The closures trigger unnecessary re-renders.
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
Try looking for all closures that could be causing re-renders and make sure components don't depend on them.
Edit:
Ben West is right- you also have side effects happening during the render, which is wrong.
When you have something like this in the body of a functional component:
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
Change it to this:
useEffect(() => {
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
}, [status, userData]); //the function in arg 1 is called whenever these dependencies change
I made a bunch of changes to your code:
It's down to 2 files. The other stuff I inlined.
I'm not that familiar with useContext(), so I can't say if you're using it correctly.
Login.js:
import { useContext, useEffect } from 'react';
import { makeStyles } from '#material-ui/core';
import Container from '#material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import AuthContext from '../store/auth-context';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function Login() {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = async requestData => {
dispatch({ type: 'SEND' });
try {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: requestData.email,
password: requestData.password,
},
};
const response = await axios(config);
dispatch({ type: 'SUCCESS', responseData: response.data });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
};
const authCtx = useContext(AuthContext);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
useEffect(() => {
if (httpState.status === 'pending') {
console.log('making request');
}
}, [httpState.status]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.data) {
console.log('updateContext');
authCtx.login(httpState.data);
}
}, [httpState.status, httpState.data]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.error) {
console.log(httpState.error);
}
}, [httpState.status, httpState.error]);
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={httpState.status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
AuthContext.js:
import React, { useState } from 'react';
import { useEffect } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
// is this asynchronous?
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const [tokenData, setTokenData] = useState(null);
const [logoutTimer, setLogoutTimer] = useState(null);
useEffect(() => {
const tokenData_ = retrieveStoredToken(); //is this asynchronous?
if (tokenData_) {
setTokenData(tokenData_);
}
}, []);
const userIsLoggedIn = !!(tokenData && tokenData.token);
const logoutHandler = () => {
setTokenData(null);
removeUser();//is this asynchronous?
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
//clear logoutTimer state here? -> setLogoutTimer(null);
}
};
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setTokenData({ token });
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
setLogoutTimer(setTimeout(logoutHandler, remainingTime));
};
useEffect(() => {
if (tokenData && tokenData.duration) {
console.log(tokenData.duration);
setLogoutTimer(setTimeout(logoutHandler, tokenData.duration));
}
}, [tokenData]);
const user = {
token: tokenData.token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;
I am trying to use redux in my app for the first time and i set up whole boilerplate for it but i got an error which says Error: The slice reducer for key "AuthReducer" returned undefined during initialization. as it says i checked the state value passed in reducer but i can't figure it out, Here i am adding my code sample.
reducer.js
import {combineReducers} from 'redux'
import { SIGN_IN, SIGN_OUT, SIGN_UP, MOVIE_ADDED, MOVIE_REMOVED } from './ActionTypes'
const initialMovieState = {
savedMovies: [
{
id: '',
name: ''
}
]
}
const initialAuthState = {
userEmail: '',
userPassword: '',
userToken: '',
}
export const AuthReducer = (state=initialAuthState, action) => {
switch (action.type) {
case SIGN_IN:
return {
...state,
userToken: action.payload.token
}
case SIGN_UP:
return {
...state,
userEmail: action.payload.email,
userPassword: action.payload.password
}
case SIGN_OUT:
return {
...state,
userToken: ''
}
default:
state
}
}
export const MovieReducer = (state=initialMovieState, action) => {
switch (action.type) {
case MOVIE_ADDED:
return {
...state,
savedMovies: state.savedMovies.concat({
id: action.payload.movieId,
name: action.movieName
})
}
case MOVIE_REMOVED:
return state.filter(movie => movie.id !== action.payload.movieId)
default:
state
}
}
const RootReducer = combineReducers({
AuthReducer,
MovieReducer
})
export default RootReducer
actions.js
import {SIGN_IN, SIGN_OUT, SIGN_UP, MOVIE_ADDED, MOVIE_REMOVED} from './ActionTypes'
export const sign_in = (user) => (
{
type: SIGN_IN,
payload: {
token: user
}
}
)
export const sign_up = (user) => (
{
type: SIGN_UP,
payload: {
email: user.email,
password: user.password
}
}
)
export const sign_out = () => (
{
type: SIGN_OUT,
}
)
export const movie_added = (movie) => (
{
type: MOVIE_ADDED,
payload: {
movieId: movie.id,
movieName: movie.name,
}
}
)
export const movie_removed = () => (
{
type: MOVIE_REMOVED
}
)
store.js
import { createStore } from 'redux'
import {composeWithDevTools} from 'redux-devtools-extension'
import rootReducer from './Reducer'
const Store = createStore(rootReducer, composeWithDevTools())
export default Store
I've wrapped the whole app in Provider with store as an argument in App.js
SignUpScreen.js
import {useDispatch, useStore} from 'react-redux'
import {sign_up} from '../redux/Actions'
const SignUpScreen = () => {
const navigation = useNavigation()
const dispatch = useDispatch()
const Store = useStore()
...
const handleSubmit = () => {
if(isValidEmail === true && isValidPass === true && isPassMatched === true){
console.log(Store.getState())
dispatch(sign_up({email: email, password: password}))
console.log(Store.getState())
navigation.navigate('signin')
ToastAndroid.showWithGravity(
'Account created.',
ToastAndroid.LONG,
ToastAndroid.BOTTOM
)
} else {
ToastAndroid.showWithGravity(
'Invalid Email or Password',
ToastAndroid.LONG,
ToastAndroid.BOTTOM
)
}
}
...
return(
{/* register button */}
<TouchableOpacity style={styles.regBtnCont} onPress={() => handleSubmit()}>
<Text style={styles.regBtnText}>Register</Text>
</TouchableOpacity>
)
}
Any help will be appriciated, Thank you.
Im working on a calendar app using React and Redux.
The calendar app fetch dates and their events from google api, so users could see their google calendars.
In the debugger i can see that the data is ok all the way from the dispatch to the render function. the only problem is that the component inside (MonthlyCalendar) that gets the events getting an empty object.
when not using redux and instead just react state, the component do get the new props with the updated events object.
I added console.log to each life cycle of the component.
When using redux i get this:
using regular state (not redux):
i don't know what I'm missing with redux flow but when debugging it's shown that render is executed with the updated state events,
still the console.log print empty.
The code from dispatch to render :
Dispatch
useEffect(() => {
window.addEventListener(
'message',
e => {
if (e.data && e.data.data) {
dispatch(connectBtnClicked(e.data.data));
}
},
false,
);
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
}, [])
Action
export const connectBtnClicked = (userName: string) => (dispatch: any) => {
const urlArray = [
'https://calendar-server.codev.co.il/getEvents',
'https://calendar-server.codev.co.il/getCalendarsListIds',
'https://calendar-server.codev.co.il/getSettings',
];
const requestsArray = urlArray.map(url => {
const request = new Request(url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
method: 'GET',
});
return fetch(request).then(res => res.json());
});
Promise.all(requestsArray).then(allResults => {
console.log('***allResults', allResults);
dispatch({
type: CONNECT_BTN_CLICKED,
payload: {
isConnect: true,
userName,
dates: allResults[0].eventsByDates,
calendarsList: allResults[1].calendarsList,
},
});
});
};
Reducer
import {
CONNECT_BTN_CLICKED,
SET_COMP_ID,
DISCONNECT_BTN_CLICKED,
CONNECT_ERROR,
} from '../actions/actionType';
const initState = {
isConnect: false,
isDisconnect: false,
isSettingsLoaded: false,
userName: '',
isLoader: false,
connectError: false,
statusCode: '',
dates: {},
calendarsList: [],
};
export default (state = initState, action: any) => {
console.log('setReducer', action);
switch (action.type) {
case SET_COMP_ID:
return {
...state,
compId: action.payload,
};
case CONNECT_ERROR:
return {
...state,
connectEtsyError: action.payload.err,
statusCode: action.payload.statusCode,
};
case CONNECT_BTN_CLICKED:
return {
...state,
isConnect: action.payload.isConnect,
userName: action.payload.userName,
dates: action.payload.dates,
calendarsList: action.payload.calendarsList,
isSettingsLoaded: true,
};
case DISCONNECT_BTN_CLICKED:
return {
...state,
isConnect: false,
// isDisconnect: action.payload,
dates: {},
calendarsList: [],
userName: '',
};
default:
return state;
}
};
Root Reducer
import { combineReducers } from 'redux';
//#ts-ignore
import settings from './settingsReducer';
const rootReducer = combineReducers({
settings,
});
export default rootReducer;
Store
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers/rootReducer';
import thunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
Provider Wrapping App.js Component
import { BrowserRouter as Router } from 'react-router-dom';
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import { I18nextProvider } from 'react-i18next';
import App from './components/App/App';
// import App from './components/App';
import i18n from './i18n';
import { Provider } from 'react-redux';
import store from './components/settings/store';
const locale = window.__LOCALE__;
const baseURL = window.__BASEURL__;
fedopsLogger.appLoaded();
ReactDOM.render(
<Provider store={store}>
<React.Suspense fallback={<div>Please wait...</div>}>
<Router>
{/* <ExperimentsProvider options={{ experiments }}> */}
<App />
{/* </ExperimentsProvider> */}
</Router>
</React.Suspense>
</Provider>,
document.getElementById('root'),
);
App.js render - MonthlyCalendar is the events receiving component
render() {
const { t } = this.props;
const events = this.props.events;
return (
<Switch>
<Route
path="/index"
render={() => (
<MonthlyCalendar
weekStarter={this.state.weekStarter}
events={events}
handleMonthChange={handleMonthChange}
isTimeZoneShown={this.state.isTimeZoneShown}
isTimeShown={this.state.isTimeShown}
locale={this.state.locale}
//timeZone={this.state.timeZone}
isTodayButtonStyleSeconday={this.state.isTodayButtonStyleSeconday}
/>
)}
></Route>
<Route
path="/settings"
render={() => (
<Settings
fetchEvents={this.fetchEvents}
initialState={"this.props.initialState"}
/>
)}
></Route>
<Route
path="/mobile"
render={() => (
<MonthlyCalendar
weekStarter={this.state.weekStarter}
events={events}
handleMonthChange={handleMonthChange}
isTimeZoneShown={this.state.isTimeZoneShown}
isTimeShown={this.state.isTimeShown}
locale={this.state.locale}
isTodayButtonStyleSeconday={this.state.isTodayButtonStyleSeconday}
/>
)}
></Route>
</Switch>
);
}
}
const mapDispatchToProps = (dispatch: any) => ({});
const mapStateToProps = (state: any) => ({
isConnect: state.settings.isConnect,
userName: state.settings.userName,
events: state.settings.dates,
calendarsList: state.settings.calendarsList,
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withTranslation()(withEnhancedStyleLoader(App)));
There is not setState in componentDidMount or any other method in App.js I removed it all, so only props change could re-render
Edit 1
i dropped all the HOCS and still in render i see the updated events, but the child component print empty events.
Render on events received
EDIT 2
the child component only render once. even when events props is updated, and re-render MonthlyCalendar( the child component ) doesn't re-render. I tried:
const events = {...this.props.events};
also:
const events = JSON.parse(JSON.stringfy(this.props.events);
didn't work...
EDIT 3 - MonthlyCalendar Component
constructor(props: any) {
super(props);
console.log('[constructor] props.events: ',props.events)
const timezone = moment.tz.guess();
const dateObject = moment().tz(timezone, true);
this.state = {
dateObject,
timezone,
isTimezonesOpen: false,
};
}
shouldComponentUpdate(nextProps, nextState) {
console.log('[shouldComponentUpdate] props.events: ',this.props.events)
return true
}
....
getCalendar() {
const { events } = this.props;
const { dateObject } = this.state;
const beforeFillers = this.getMonthBeforFillers(dateObject, events);
const days = this.getDays(dateObject, events);
const afterFillers = this.hasAfterFillers(beforeFillers, days) ?
this.getAfterMonthFillers(dateObject, events) : {};
return { days, beforeFillers, afterFillers };
}
async componentDidUpdate(prevProps) {
console.log('[componentDidUpdate] props.events: ',this.props.events)
this.props.locale !== prevProps.locale && await this.updateLocale();
}
updateLocale = async () => {
const { locale, i18n } = this.props;
await i18n.changeLanguage(locale);
moment.locale(locale);
const { timezone, dateObject } = this.state;
const dateObjectToSet = moment(dateObject.format()).tz(timezone, true);
this.setState({ dateObject: dateObjectToSet });
}
async componentDidMount() {
console.log('[componentDidMount] props.events: ',this.props.events)
this.props.locale !== 'en' && await this.updateLocale();
}
render() {
const { t, weekStarter, isTodayButtonStyleSeconday, isTimeZoneShown, isTimeShown } = this.props;
const { dateObject, timezone, isTimezonesOpen } = this.state;
const { days, beforeFillers, afterFillers } = this.getCalendar();
const month = dateObject.format(t('Google_Calendar_Picker_Month'));
const timezoneSelected = moment().tz(timezone).format(t('Google_Calendar_Timezone_Selected'));
const timezoneSelectedTitle = t('Google_Calendar_Timezone_Selected_Title', { timezoneSelected });
console.log('[render] props.events: ',this.props.events)
return (
<TPAComponentsProvider value={{ mobile: false, rtl: false }}>
<div className={classes.MonthlyCalendar}>
<CalendarControllers
isTodayButtonStyleSeconday={isTodayButtonStyleSeconday}
todayClicked={this.todayClickedHander}
onPreviousClicked={() => this.timePickerClickedHandler(false)}
timeToDisplay={month}
onNextClicked={() => this.timePickerClickedHandler(true)}
onTimezoneChange={this.timezoneChangeHandler}
timezone={timezoneSelectedTitle}
isTimezonesOpen={isTimezonesOpen}
openTimezones={this.openTimezones}
closeTimezones={this.closeTimezones}
isTimeZoneShown={isTimeZoneShown}
/>
<MonthTable
weekStarter={weekStarter}
days={days}
beforeFillers={beforeFillers}
dateObject={dateObject}
afterFillers={afterFillers}
renderCell={(
time: any,
events: any,
cellRef: any,
handleEventClick: any,
setExpendedEvent: any,
expendedEvent: any,
isOutsideClicked: any,
) => (
<MonthlyCell
events={events}
handleEventClick={handleEventClick}
time={time}
cellRef={cellRef}
expendedEvent={expendedEvent}
isOutsideClicked={isOutsideClicked}
setExpendedEvent={setExpendedEvent}
isTimeShown={isTimeShown}
/>
)}
/>
</div>
</TPAComponentsProvider>
);
}
}
export default withTranslation()(MonthlyCalendar);
EDIT 4
after searching for solutions i added key to the div of MonthlyCalendar and also i added destructor {...this.props.events}. still no re-render
Following is the updated MonthlyCalendar:
getCalendar() {
const mutableEvents = {...this.props.events};
const { dateObject } = this.state;
const beforeFillers = this.getMonthBeforFillers(dateObject, mutableEvents);
const days = this.getDays(dateObject, mutableEvents);
const afterFillers = this.hasAfterFillers(beforeFillers, days) ?
this.getAfterMonthFillers(dateObject, mutableEvents) : {};
return { days, beforeFillers, afterFillers };
}
async componentDidUpdate(prevProps) {
console.log('[componentDidUpdate] props.events: ',this.props.events)
this.props.locale !== prevProps.locale && await this.updateLocale();
}
updateLocale = async () => {
const { locale, i18n } = this.props;
await i18n.changeLanguage(locale);
moment.locale(locale);
const { timezone, dateObject } = this.state;
const dateObjectToSet = moment(dateObject.format()).tz(timezone, true);
this.setState({ dateObject: dateObjectToSet });
}
async componentDidMount() {
console.log('[componentDidMount] props.events: ',this.props.events)
this.props.locale !== 'en' && await this.updateLocale();
}
render() {
const { t, weekStarter, isTodayButtonStyleSeconday, isTimeZoneShown, isTimeShown, events: propEvents } = this.props;
const eventsKey = Object.keys(propEvents).length;
const { dateObject, timezone, isTimezonesOpen } = this.state;
const { days, beforeFillers, afterFillers } = this.getCalendar();
const month = dateObject.format(t('Google_Calendar_Picker_Month'));
const timezoneSelected = moment().tz(timezone).format(t('Google_Calendar_Timezone_Selected'));
const timezoneSelectedTitle = t('Google_Calendar_Timezone_Selected_Title', { timezoneSelected });
console.log('[render] props.events: ',this.props.events)
return (
<TPAComponentsProvider value={{ mobile: false, rtl: false }}>
<div key={eventsKey} className={classes.MonthlyCalendar}>
<CalendarControllers
isTodayButtonStyleSeconday={isTodayButtonStyleSeconday}
todayClicked={this.todayClickedHander}
onPreviousClicked={() => this.timePickerClickedHandler(false)}
timeToDisplay={month}
onNextClicked={() => this.timePickerClickedHandler(true)}
onTimezoneChange={this.timezoneChangeHandler}
timezone={timezoneSelectedTitle}
isTimezonesOpen={isTimezonesOpen}
openTimezones={this.openTimezones}
closeTimezones={this.closeTimezones}
isTimeZoneShown={isTimeZoneShown}
/>
<MonthTable
weekStarter={weekStarter}
days={days}
beforeFillers={beforeFillers}
dateObject={dateObject}
afterFillers={afterFillers}
renderCell={(
time: any,
events: any,
cellRef: any,
handleEventClick: any,
setExpendedEvent: any,
expendedEvent: any,
isOutsideClicked: any,
) => (
<MonthlyCell
events={events}
handleEventClick={handleEventClick}
time={time}
cellRef={cellRef}
expendedEvent={expendedEvent}
isOutsideClicked={isOutsideClicked}
setExpendedEvent={setExpendedEvent}
isTimeShown={isTimeShown}
/>
)}
/>
</div>
</TPAComponentsProvider>
);
}
}
export default withTranslation()(MonthlyCalendar);
GIF of Rerender occuring
I'm not sure how to proceed. As you can see, the Header's state (as passed down via context) is switching from the user's data --> undefined --> same user's data. This occurs every time there's a url change, and doesn't happen when I do things that don't change the url (like opening the cart for example).
Is this expected behaviour? Is there any way I can get the query in my context to only be called when there is no user data or when the user data changes? I tried using useMemo, but to no avail.
auth.context
import React, { useState} from "react";
import {
CURRENT_USER,
GET_LOGGED_IN_CUSTOMER,
} from "graphql/query/customer.query";
import { gql, useQuery, useLazyQuery } from "#apollo/client";
import { isBrowser } from "components/helpers/isBrowser";
export const AuthContext = React.createContext({});
export const AuthProvider = ({ children }) => {
const [customer, { data, loading, error }] = useLazyQuery(
GET_LOGGED_IN_CUSTOMER,
{
ssr: true,
}
);
const { data: auth } = useQuery(CURRENT_USER, {
onCompleted: (auth) => {
console.log(auth);
customer({
variables: {
where: {
id: auth.currentUser.id,
},
},
});
},
ssr: true,
});
console.log(data);
const isValidToken = () => {
if (isBrowser && data) {
const token = localStorage.getItem("token");
if (error) {
console.log("error", error);
}
if (token && data) {
console.log("token + auth");
return true;
} else return false;
}
};
const [isAuthenticated, makeAuthenticated] = useState(isValidToken());
function authenticate() {
makeAuthenticated(isValidToken());
}
function signout() {
makeAuthenticated(false);
localStorage.removeItem("token");
}
return (
<AuthContext.Provider
value={{
isAuthenticated,
data,
authenticate,
auth,
signout,
}}
>
{children}
</AuthContext.Provider>
);
};
(In Header, userData is equal to data just passed through an intermediary component (to provide to mobile version)).
header.tsx
import React, { useContext } from "react";
import Router, { useRouter } from "next/router";
import { useApolloClient } from "#apollo/client";
import { openModal } from "#redq/reuse-modal";
import SearchBox from "components/SearchBox/SearchBox";
import { SearchContext } from "contexts/search/search.context";
import { AuthContext } from "contexts/auth/auth.context";
import LoginModal from "containers/LoginModal";
import { RightMenu } from "./Menu/RightMenu/RightMenu";
import { LeftMenu } from "./Menu/LeftMenu/LeftMenu";
import HeaderWrapper from "./Header.style";
import LogoImage from "image/hatchli-reduced-logo.svg";
import { isCategoryPage } from "../is-home-page";
type Props = {
className?: string;
token?: string;
pathname?: string;
userData?: any;
};
const Header: React.FC<Props> = ({ className, userData }) => {
const client = useApolloClient();
const { isAuthenticated, signout } = useContext<any>(AuthContext);
const { state, dispatch } = useContext(SearchContext);
console.log(isAuthenticated);
console.log(userData);
const { pathname, query } = useRouter();
const handleLogout = () => {
if (typeof window !== "undefined") {
signout();
client.resetStore();
Router.push("/medicine");
}
};
const handleJoin = () => {
openModal({
config: {
className: "login-modal",
disableDragging: true,
width: "auto",
height: "auto",
animationFrom: { transform: "translateY(100px)" },
animationTo: { transform: "translateY(0)" },
transition: {
mass: 1,
tension: 180,
friction: 26,
},
},
component: LoginModal,
componentProps: {},
closeComponent: "",
closeOnClickOutside: true,
});
};
const onSearch = (text: any) => {
dispatch({
type: "UPDATE",
payload: {
...state,
text,
},
});
};
const { text } = state;
const onClickHandler = () => {
const updatedQuery = query.category
? { text: text, category: query.category }
: { text };
Router.push({
pathname: pathname,
query: updatedQuery,
});
};
const showSearch = isCategoryPage(pathname);
return (
<HeaderWrapper className={className}>
<LeftMenu logo={LogoImage} />
{showSearch && (
<SearchBox
className="headerSearch"
handleSearch={(value: any) => onSearch(value)}
onClick={onClickHandler}
placeholder="Search anything..."
hideType={true}
minimal={true}
showSvg={true}
style={{ width: "100%" }}
value={text || ""}
/>
)}
<RightMenu
isAuth={userData}
onJoin={handleJoin}
onLogout={handleLogout}
avatar={userData && userData.user && userData.user.avatar}
/>
</HeaderWrapper>
);
};
export default Header;
React/Redux application goes into an infinite loop on using useEffect with object references..
I am trying render pending todos for my application using useEffect.. and passing the array of todos as the second param in useEffect ..but why is not checking the values of the object ?
Container:
const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(RootActions, dispatch) });
const Home = (props) => {
const { root, actions } = props;
useEffect(() => {
getTodos(actions.loadPendingTodo);
}, [root.data]);
return (
<Segment>
<Error {...root } />
<TodoList { ...root } actions={actions} />
</Segment>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Action:
export const loadPendingTodo = () => ({
type: LOAD_PENDING_TODO,
data: todoService.loadPendingTodo(),
});
Reducer:
const initialState = {
initial: true,
data: [{
id: 0,
text: 'temp todo',
dueDate: new Date(),
completedDate: '',
isDeleted: false,
isCompleted: false,
}],
error: false,
isLoading: false,
isEdit: false,
};
export default function root(state = initialState, action) {
switch (action.type) {
case LOAD_PENDING_TODO:
return {
...state,
data: [...action.data],
};
...
default:
return state;
}
}
getTodos Method:
export const getTodos = (loadTodo) => {
try {
loadTodo();
} catch (error) {
console.log(error); // eslint-disable-line
}
};
Service:
export default class TodoAppService {
loadPendingTodo() {
return store.get('todoApp').data.filter(todo => !todo.isCompleted && !todo.isDeleted);
}
Can anyone please help me out how to resolve this issue.. and there is no official documentation for this case too :/
Moreover changing the useEffect to the following works but i want to render on every change
useEffect(() => {
getTodos(actions.loadPendingTodo);
}, []);
Fixed it by removing the loadPedningTodo redux actions in useEffect that was causing it to loop and directly setting the data in function from service..
const Home = (props) => {
const { root, actions } = props;
return (
<Segment>
<Error {...root } />
<TodoList isEdit={root.isEdit} todo={todoService.loadPendingTodo()} actions={actions} />
</Segment>
);
};
thanks :)