Update site immediately after setting Local Storage - reactjs

I am making a mern stack application and currently I am trying to switch between the login route and main page depending if you are logged in or not. However, this only works once I refresh the page, is there any way I can make it work without having to refresh the page?
App.js
{!localStorage.getItem('token') ? (
<Redirect exact from='/' to='/login' />
):
<>
<Navbar />
<Redirect to='/' />
</>
}

Reacting to changes in local storage is -at best- a weird approach. In practice, the only way for a component to re-render, is by the props that it receives to change, or by using component state via useState.
I'll write this imaginary piece of code to illustrate my point:
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
// ...
const LoginPage = _props {
const [token, setToken] = useState(localStorage.getItem('token'))
if (token) {
return <Redirect to='/' />
}
// I have no idea how you login your users
return (
<div>
<LoginForm onToken={setToken} />
</div>
)
}
If you need component A to react to changes done by component B, where neither of them is a direct child of the other, you will need global state.
Global state is similar to component state in that changes on it should trigger a re-render on the component that depends on it. But it is global, not local to a particular component.
To achieve this, there are complex solutions like redux, but you can implement a very simple version of it using a React Context:
// src/providers/GlobalStateProvider.js
import React, { createContext, useContext, useState } from 'react'
const Context = createContext()
const GlobalStateProvider = ({ children }) => {
const [token, doSetToken] = useState(localStorage.getItem('token'))
const setToken = t => {
doSetToken(t)
localStorage.setItem('token', token)
}
return (
<Context.Provider value={{ token, setToken }}>
{children}
</Context>
)
}
export { Context }
export default GlobalStateProvider
// src/App.js
import GlobalStateProvider from './providers/GlobalStateProvider'
// ...
const App = _props => {
return (
{/* Any component that is descendant of this one can access the context values, an will re-render if they change */}
<GlobalStateProvider>
{/* ... the rest of your components */}
</GlobalStateProvider>
)
}
// ...
// your particular component
import React, { useContext } from 'react'
import { Context } from 'path/to/GlobalStateProvider'
const SomeComponent = _props => {
// Component will re-render if token changes
// you can change token from wherever by using `setToken`
const { token, setToken } = useContext(Context)
if (token) {
// do this
} else {
// do that
}
}

Related

Why is my state not updating when called in useEffect()?

Have an annoying little bug in my application, which I am not sure is thanks to Redux states or something wrong with how I updated a internal state in React in a useEffect(). Either way I cannot seem to find an answer
I have a few console.log() throughout to try to better understand this.
I have a state in my App component, const [loading,setLoading] = useState(true) which determined where the page is loading, and if it is, returns a "loading" message on screen until all operations are completed. Inside my useEffect, depending on whether it is able to find a user in local storage, updates the state loading to false if there is a user found or if there isn't. When loading is false, the rest of applications routes are able to be rendered.
The issue I am having though, is that it seems as if my state does not initially update inside useEffect?
I do not have Strict mode because I am trying to see whats going on.
I have included a pic of my console logs, to give you an idea. As you can see after the useEffect is ran, even though it should updated loading state to false, upon rendering it has not updated, but somehow the dispatch inside useEffect has ran, because it shows the redux state has updated. Makes no sense to me.
After that, the App is loaded again, and somehow the loading state is not correct? Makes no sense to me
// React
import { useEffect, useState } from 'react'
// Redux
import { useSelector, useDispatch } from 'react-redux'
// React Reducer
import { Routes, Route } from "react-router-dom"
import { loginUser, logoutUser } from './reducers/user';
// Components
import Login from './components/Login';
import Player from './components/Player';
function App() {
console.log("app start")
const dispatch = useDispatch()
const user = useSelector(state => state.user)
console.log("user:",user)
const [loading,setLoading] = useState(true)
console.log("state:", loading)
useEffect(() => {
console.log("in app effet")
const loggedUserJSON = window.localStorage.getItem('loggedUser')
if (loggedUserJSON) {
console.log("inside json conditional")
const user= JSON.parse(loggedUserJSON)
dispatch(loginUser(user))
setLoading(false)
return
}
setLoading(false)
}, [])
// If loading is false
if (loading === false) {
return (
<div>
{user && <h1><button onClick={() => dispatch(logoutUser())}>LOG OUT</button></h1>}
<Routes>
<Route path="/" element={!user ? <Login/> : <h1>Home</h1>} />
<Route path="/player/:username" element={<Player/>}/>
{user && <Route path ='/store' element={<h1>STORE</h1>}/>}
</Routes>
</div>
)
}
console.log("end of app component")
// If Loading is true, not updated
return (
<h1>LOADING</h1>
);
}
export default App;
Try this, you may not get the correct value of loading just by console.log as batch process happens for the set state.
// React
import { useEffect, useState } from 'react'
// Redux
import { useSelector, useDispatch } from 'react-redux'
// React Reducer
import { Routes, Route } from "react-router-dom"
import { loginUser, logoutUser } from './reducers/user';
// Components
import Login from './components/Login';
import Player from './components/Player';
function App() {
console.log("app start")
const dispatch = useDispatch()
const {user} = useSelector(state => state.user)
console.log("user:",user)
const [loading,setLoading] = useState(true)
console.log("state:", loading)
useEffect(() => {
console.log("in app effet")
const loggedUserJSON = window.localStorage.getItem('loggedUser')
if (loggedUserJSON) {
console.log("inside json conditional")
const user= JSON.parse(loggedUserJSON)
dispatch(loginUser(user))
//setLoading(false)
}
setLoading(false)
}, [])
// If loading is false
if (!loading) {
return (
<div>
{user && <h1><button onClick={() => dispatch(logoutUser())}>LOG OUT</button></h1>}
<Routes>
<Route path="/" element={!user ? <Login/> : <h1>Home</h1>} />
<Route path="/player/:username" element={<Player/>}/>
{user && <Route path ='/store' element={<h1>STORE</h1>}/>}
</Routes>
</div>
)
}
else
return <h1>LOADING</h1>
// console.log("end of app component")
// If Loading is true, not updated
}
export default App;

React - Warning: Cannot update a component (`PrivateRoute`) while rendering a different component (`Modules`)

Getting the following error on all child components.
react-dom.development.js:86 Warning: Cannot update a component
(PrivateRoute) while rendering a different component (Example). To
locate the bad setState() call inside Examples,
I've found lots of examples of the same error but thus far no solutions
React Route Warning: Cannot update a component (`App`) while rendering a different component (`Context.Consumer`)
Can Redux cause the React warning "Cannot update a component while rendering a different component"
The PrivateRoute wraps the component to redirect if not logged in.
export default function PrivateRoute() {
const session: ISessionReducer = useSelector((state: RootState) => state.core.session);
useEffect(() => {
if (!session.jwt) <Navigate to="/login" />;
}, [session]);
return <Outlet />;
};
It is happening because useEffect runs after the component is rendered. So what's happening in this case is that your Outlet component is getting rendered first before your code in useEffect runs. So if the jwt token doesn't exist then it will try to redirect but it won't be able to because your Outlet will already be rendered by then.
So I can give you the solution of what I use to check if the jwt token exist.
1.) I create a custom hook for checking if the token exists.
2.) And then I use that custom hook in my privateRoute component to check if the user is loggedIn.
useAuthStatus.js
import { useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
export const useAuthStatus = () => {
const [loggedIn, setLoggedIn] = useState(false)
const [checkingStatus, setCheckingStatus] = useState(true)
const { user } = useSelector((state) => state.auth)
useEffect(() => {
if (user?.token) {
setLoggedIn(true)
} else {
setLoggedIn(false)
}
setCheckingStatus(false)
}, [user?.token])
return { loggedIn, checkingStatus }
}
PrivateRoute component
import { Navigate, Outlet } from 'react-router-dom'
import { useAuthStatus } from '../../hooks/useAuthStatus'
import CircularProgress from '#mui/material/CircularProgress'
const PrivateRoute = () => {
const { loggedIn, checkingStatus } = useAuthStatus()
if (checkingStatus) {
return <CircularProgress className='app__modal-loader' />
}
return loggedIn ? <Outlet /> : <Navigate to='/login' />
}
export default PrivateRoute

NextJS: Context values undefined in production (works fine in development)

A "dark mode" feature has been implemented on my Next.js application using React's Context api.
Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.
_app.tsx is wrapped with the ThemeProvider as such:
// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
// Irrelevant imports
// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";
// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";
// Irrelevant code
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// Applying different layouts depending on page
switch (Component.name) {
case "HomePage":
return (
<Provider store={store}>
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
<Footer color="fff" />
</AuthProvider>
</ThemeProvider>
</Provider>
);
case "PageNotFound":
return (
<>
<Component {...pageProps} />
<Footer color="#f2f2f5" />
</>
);
default:
// Irrelevant code
}
}
export default MyApp;
The ThemeContext correctly exports both its Provider and Context:
import { createContext, ReactNode, useState, useEffect } from "react";
type themeContextType = {
darkMode: boolean | null;
toggleDarkMode: () => void;
};
type Props = {
children: ReactNode;
};
// Checks for user's preference.
const getPrefColorScheme = () => {
return !window.matchMedia
? null
: window.matchMedia("(prefers-color-scheme: dark)").matches;
};
// Gets previously stored theme if it exists.
const getInitialMode = () => {
const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.
// If mode was saved ► return saved mode else get users general preference.
return isReturningUser ? savedMode : userPrefersDark ? true : false;
};
export const ThemeContext = createContext<themeContextType>(
{} as themeContextType
);
export const ThemeProvider = ({ children }: Props) => {
// localStorage only exists on the browser (window), not on the server
const [darkMode, setDarkMode] = useState<boolean | null>(null);
// Getting theme from local storage upon first render
useEffect(() => {
setDarkMode(getInitialMode);
}, []);
// Prefered theme stored in local storage
useEffect(() => {
localStorage.setItem("dark-mode", JSON.stringify(darkMode));
}, [darkMode]);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
{children}
</ThemeContext.Provider>
);
};
The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):
import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";
const ThemeToggler: FC = () => {
const { darkMode, toggleDarkMode } = useContext(ThemeContext);
const toggleTheme = () => {
console.log(darkMode) // <--- darkMode is undefined during production
toggleDarkMode();
};
return (
<div className="theme-toggler">
<i
className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
data-testid="dark-mode"
onClick={toggleTheme}
></i>
</div>
);
};
export default ThemeToggler;
The solutions/suggestions I've looked up before posting this were to no avail.
React Context API undefined in production — react and react-dom are on the same version.
Thanks in advance.
P.S. For those wondering why I am using both Redux and Context for global state management:
Context is best suited for low-frequency and simple state updates such as themes and authentication.
Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.
P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.
Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.
Here's your topology of the site
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.
Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
You get the point, you want to first go through a layer that theme always exist before you fire up a router.
You can confirm if this is the case by doing the following test.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.

Simple logout component throws "Cannot update a component from inside the function body of a different component"

This little Logout.jsx component logs-out the user...
import React from 'react';
import { Redirect } from 'react-router';
import { useDispatch } from 'react-redux';
import { userLogout } from '../redux/actions/authActions';
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
dispatch(userLogout());
return <Redirect to={to} />;
};
export default Logout;
and is used in path /logout thus:
<Switch>
...
<Route exact path="/logout" component={Logout} />
In the console it gives the dreaded (and apparently serious) message:
Cannot update a component from inside the function body of a different
component
Can someone spot why, and how to fix it?
Using react 16.13.0
I think it's just a logical mistake causing this error to pop up from another component (than Logout), try logging out once:
const Logout = ({ to = '/loginForm' }) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(userLogout());
}, [dispatch]);
return <Redirect to={to} />;
};
You don't want to dispatch (or logout) on every component render

Share API data with other components by using React Hook, NO REDUX

I'm experimenting with Hooks without any state management tool (such as Redux), to get the same kind of behavior/structure I could have by using a traditional structure of classes + redux.
Usually, with a class base code I would:
ComponentDidMount dispatch to Call the API
Use actions and reducers to store the data in Redux
Share the data to any component I want by using mapStateToProps
And here where the problem is by using Hooks without Redux: 'Share the DATA with any component'.
The following example is the way I have found to share states between components by Hooks:
//app.js
import React, { useReducer } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Routes from '../../routes'
import Header from '../Shared/Header'
import Footer from '../Shared/Footer'
export const AppContext = React.createContext();
// Set up Initial State
const initialState = {
userType: '',
};
function reducer(state, action) {
switch (action.type) {
case 'USER_PROFILE_TYPE':
return {
userType: action.data === 'Student' ? true : false
};
default:
return initialState;
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} />
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
)
}
export default App
// profile.js
import React, { useEffect, useState, useContext} from 'react'
import { URLS } from '../../../constants'
import ProfileDeleteButton from './ProfileDeleteButton'
import DialogDelete from './DialogDelete'
import api from '../../../helpers/API';
// Import Context
import { AppContext } from '../../Core'
const Profile = props => {
// Share userType State
const {state, dispatch} = useContext(AppContext);
const userType = type => {
dispatch({ type: 'USER_PROFILE_TYPE', data: type }); <--- Here the action to call the reducer in the App.js file
};
// Profile API call
const [ profileData, setProfileData ] = useState({});
useEffect(() => {
fetchUserProfile()
}, [])
const fetchUserProfile = async () => {
try {
const data = await api
.get(URLS.PROFILE);
const userAttributes = data.data.data.attributes;
userType(userAttributes.type) <--- here I am passing the api response
}
catch ({ response }) {
console.log('THIS IS THE RESPONSE ==> ', response.data.errors);
}
}
etc.... not important what's happening after this...
now, the only way for me to see the value of userType is to pass it as a prop to the <Header /> component.
// app.js
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} /> <--passing here the userType as prop
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
Let's say that I want to pass that userType value to children of <Routes />.
Here an example:
<AppContext.Provider value={{ state, dispatch }}>
<Routes userType={state.userType} />
</AppContext.Provider>
and then, inside <Routes /> ...
const Routes = () =>
<Switch>
<PrivateRoute exact path="/courses" component={Courses} userType={state.userType} />
</Switch>
I don't like it. It's not clean, sustainable or scalable.
Any suggestions on how to make the codebase better?
Many thanks
Joe
You don't need to pass the state as a prop to every component. Using context you can gain access to state properity in your reducer inside every child component of parent Provider. Like you have already done in the Profile.js
const {state, dispatch} = useContext(AppContext);
State property here contains state property in the reducer. So you can gain access to it by state.userType
Everything you need is within your context.
The only changes I would make is spread the data instead of trying to access it one at a time something like this
<AppContext.Provider value={{ ....state, dispatch }}>
then use const context = useContext(AppContext) within the component you need to access the data.
context.userType

Resources