React Context - Reset itself to default value - javascript

I'm working on react context. I am creating a context to store the user info from the server, it is storing it fine but there is a wired situation here. Whenever I refresh the page, it reset itself to the default value which is in my case undefined and I am not sure why.
Here is the userContext
import React from "react";
const UserContext = React.createContext();
export default UserContext;
Here is the code for UserProvider
import { useEffect, useState, useContext, useMemo } from "react";
import UserContext from "./userContext";
import axios from "axios";
const UserProvider = (props) => {
const [user, setUser] = useState()
const userLogin = () => {
axios.defaults.headers.common = {'Authorization': `Bearer ${localStorage.getItem("token")}`}
axios
.get("/auth/profile")
.then((res)=>{
setUser(res.data)
})
}
const userLogout = () => {
setUser(null)
}
const providerValue = useMemo(() => ({user, userLogin, userLogout}), [user, userLogin, userLogout] )
return(
<UserContext.Provider value={providerValue}>
{ props.children }
</UserContext.Provider>
)
}
export default UserProvider
The purpose of the userLogin function is to get the info of the user. and the userLogout is to reset the user info to null.
Here is how I implemented it in the App.js
function App() {
const [token, setToken] = useState(localStorage.getItem("token"));
return (
<BrowserRouter>
<UserProvider>
<Routes>
<Route exact path="/" element={<Welcome />}></Route>
<Route path="/register" element={<Register />}></Route>
<Route path="/login" element={<Login />}></Route>
<Route path="/goal" element={<UserGoals />}></Route>
</Routes>
</UserProvider>
</BrowserRouter>
);
}
export default App;
I will not bother you with the details of the rest of the application but I will share with you how I use it in other components
const LoginForm = () => {
const {user, userLogin, userLogout} = useContext(UserContext)
}
Whenever I log in, I just simply call the function userLogin and whenever I want to access the user, I access it using user.
The problem occurs after call userLogin and then refreshing the page, the user becomes undefined. it occurs in multiple pages not only in the login page.
any help would be appreciated.

Most likely it's because you are not calling the userLogin function when the app first runs.
try adding this in UserProvider
useEffect(() => {
if (!user) {
userLogin()
}
}, [])

Related

Can't access redux actions from props when working with react router

I'm pretty new to react and redux and I have been having a problem accessing my redux actions from the props when used in conjunction with react router. I have tried lots of configurations for formatting but no matter what I try the props only have the react router functions i.e. history, match and location. I am using connected-react-router but it does not seems to be doing anything. I have been able to access the redux actions from my nav menu components so I don't think anything is wrong there.
Here is a sample of the latest configuration I tried:
const mapStateToProps = (state) => {
return { isSignedIn: state.auth.isSignedIn }
}
const ConnectedApp = connect(
mapStateToProps,
{ signOut, signIn }
)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter basename={baseUrl} history={history}>
<ConnectedApp />
</ConnectedRouter>
</Provider>,
rootElement);
and here is inside App:
class App extends Component {
static displayName = App.name;
render () {
return (
<Layout>
<Route exact path='/' component={(props) =><Home {...props}/>} />
<Route exact path='/LogInExisting' component={(props) => <LogInExisting {...props} />} />
<Route exact path='/LogInCreate' component={(props) => <LogInCreate {...props} />} />
</Layout>
);
}
}
And here is whats trying to access the signIn action:
const LogInExisting = (props) => {
const history = useHistory();
const handleSignIn = async (e) => {
e.preventDefault()
var data = {
UserName: document.getElementById('login').value,
Password: document.getElementById('password').value
}
var response = await fetch('LogInExistingUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
var respData = await response.json();
if (!respData.success) {
//TODO: Error handling
} else {
props.signIn();
history.push('/')
}
}
I feel like I am missing something obvious but I really could use some help.
You are passing the route props (history, location, and match) when using the Route component's component prop function.
<Route
exact
path='/LogInExisting'
component={(props) => <LogInExisting {...props} />}
/>
Missing is the passing through of the props that were passed to App.
<Route
exact
path='/LogInExisting'
component={(routeProps) => <LogInExisting {...routeProps} {...this.props} />}
/>
You don't want to use an anonymous function on the component prop as this will remount the routed component each time App renders. Instead, use the render prop function, it's meant for this use case. See Route render methods for more in-depth explanation.
<Route
exact
path='/LogInExisting'
render={(routeProps) => <LogInExisting {...routeProps} {...this.props} />}
/>
This being said though, you are using Redux, which is built using the React Context API, which solves the issue of "props drilling". You shouldn't be passing your redux state and actions down as props like this. Wrap your routed components in the connect HOC locally.
<Route exact path='/LogInExisting' component={LogInExisting} />
LogInExisting
const mapStateToProps = (state) => ({
isSignedIn: state.auth.isSignedIn,
});
const mapDispatchToProps = { signOut, signIn };
export default connect(mapStateToProps, mapDispatchToProps)(LogInExisting);
Since LogInExisting is a function component, you can use the useDispatch and useSelector hooks instead of the connect HOC.
import { useDispatch, useSelector } from 'react-redux';
import { signIn } from '../path/to/actions';
const LogInExisting = (props) => {
const dispatch = useDispatch();
const isSignedIn = useSelector(state => state.auth.isSignedIn);
const history = useHistory();
const handleSignIn = async (e) => {
e.preventDefault();
...
if (!respData.success) {
//TODO: Error handling
} else {
dispatch(signIn());
history.push('/');
}
}

Is getting currently authenticated user an asynchronous call?

If I get currently authenticated user through auth.currentUser, is this an asynchronous cal and be handled as such?
Namely, I have this top-level App component in react with firebase on the backend.
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import MainNav from './layouts/Navbar.js';
import Homepage from './pages/Homepage';
import Login from './pages/Login';
import Register from './pages/Register';
import Profile from './pages/Profile';
import Question from './pages/Question';
import auth from 'auth/path/from/firebase-config'
function App() {
const user = auth.currentUser
return (
<>
<BrowserRouter>
<MainNav />
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/questions/:id" element={<Question />} />
<Route path="/users/:id" element={<Profile />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Routes>
</BrowserRouter>
</>
);
}
export default App;
I would like to know if there is an authenticated user and tell that to the . Depending on whether there is a user I want to show register, and login buttons or logout button (or something like that)
Is there maybe a way here to utilize firebase's onAuthStateChanged observer?
Firebase automatically restores the user credentials when the page/app reloads. This requires it to make a call to the server, so happens asynchronously. This call likely hasn't completed when your auth.currentUser runs, which means you get null for the current user.
The solution is indeed as you say to use an auth state listener, which fires for the first time after the asynchronous call has compelte.
Yes, you must wait onAuthStateChanged to get it ready.
In my app, I created a top-level component to handle this case.
This component blocks render, until firebase get ready.
import { useCallback, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { fetchProfileDetails, handleAuthStateChanged } from '../../redux/actions/auth';
import { getAuth, onAuthStateChanged } from 'firebase/auth'
const auth = getAuth()
export default function AuthGate({ children }) {
const dispatch = useDispatch()
const authStatus = useSelector(state => state.auth.status) //idle (default, onAuthStateChanged not yet fired) || authenticated || unauthenticated
const profileStatus = useSelector(state => state.auth.profile.query.status) //idle || loading || succeeded || failed.
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, user => {
dispatch(handleAuthStateChanged(user)) // this line is responsible to change authStatus from `idle` to `authenticated` || `unauthenticated`
})
return () => {
unsubscribe()
}
}, [dispatch]);
useEffect(() => {
if (authStatus === 'authenticated') {
dispatch(fetchProfileDetails()) //fetch profile details from backend each user authenticated
} else {
// clear profile details each logout
}
}, [authStatus, dispatch]);
const renderChildren = useCallback(() => {
if (authStatus === 'idle' || profileStatus === 'idle' || profileStatus === 'loading') {
//show loading component
} else if (profileStatus === 'failed') {
//show error component
} else {
return children
}
}, [authStatus, children, profileStatus])
return renderChildren()
}
index.js
//...
ReactDOM.render(
//...
<AuthGate>
<App />
</AuthGate>
//...
,
document.getElementById('root')
);

How to fix Memory Leak when setting the state in my app

I am currently working on a music application and am running into a memory leak error message when navigating away from the details page.
When I use the back button to navigate back to my home page I get this error message in the console.
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
How do I fix this? I havn't had to deal with any code cleanup in the past while working with React.
ConcertDetails.js
import React, { useState, useEffect } from 'react';
import axios from '../utils/API';
const ConcertDetails = (Props) => {
const [details, setDetails] = useState({});
const [songs, setSongs] = useState([]);
useEffect(() => {
const url = '/concerts/' + Props.match.params.id.toString() + '/';
const getDetails = async () => {
const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
setDetails(response.data);
setSongs(response.data.song);
};
getDetails();
});
const getSongs = () => {
return songs.map((song) => (
<li key={song.title}>{song.title}</li>
));
}
return (
<>
<h1>Concert Details</h1>
<p>Venue: {details.venue}</p>
<p>Date: {details.date}</p>
<ul>
{getSongs()}
</ul>
</>
)
}
export default ConcertDetails;
App.js
import React from 'react';
import './App.css';
import NavBar from "./components/Navbar";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Home from './pages';
import About from './pages/about';
import Contact from './pages/contact';
import SignUp from './pages/signup';
import SignIn from './pages/signin';
import ConcertDetails from './pages/concertDetails';
function App() {
return (
<Router>
<NavBar />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route path="/signin" component={SignIn} />
<Route path="/signup" component={SignUp} />
<Route path="/concert/:id" component={ConcertDetails} />
</Switch>
</Router>
);
}
export default App;
You want to run useEffect only on first component render.
Add [] as the second param of useEffect.
useEffect(() => {
const url = '/concerts/' + Props.match.params.id.toString() + '/';
const getDetails = async () => {
const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
setDetails(response.data);
setSongs(response.data.song);
};
getDetails();
}, []);
without the second param. the effect will run each time state / props is changed. Since the effect changes the state, this can cause an infinite loop.
Check the plnkr: CodeSanbox: https://codesandbox.io/s/suspicious-dew-rsz5s?file=/src/App.js
The explanation:
There are multiple issues, let's solve step by step
Your useEffect is running multiple times, we have to define a condition with the array of dependencies, I'll define it only runs on the mount with empty dependencies.
But also, your useEffect is trying to setState after an async call. So, maybe at that moment, the component is already unmounted. For that reason, you have to run a safeSetState :). Basically, you have to validate before setState.
First, we will define our hook validation of mount/unmounted:
useSafeWrite.js:
import React from 'react';
function useSafeWrite(dispatch) {
const mounted = React.useRef(false)
React.useLayoutEffect(() => {
mounted.current = true
return () => (mounted.current = false)
}, [])
return React.useCallback(
(...args) => (mounted.current ? dispatch(...args) : void 0),
[dispatch],
)
}
export default useSafeWrite;
And after that, we will redefine the setDetails and setSongs with our hook and use them inside our useEffect.
ConcertDetails
const [details, setDetails] = useState({});
const [songs, setSongs] = useState([]);
const safeSetDetails = useSafeWrite(setDetails);
const safeSetSongs = useSafeWrite(setSongs);
useEffect(() => {
const getDetails = async () => {
const url = '/concerts/' + Props.match.params.id.toString() + '/';
const response = await axios.get(url, {}, {'Content-Type': 'application/json'});
safeSetDetails(response.data);
safeSetSongs(response.data.song );
};
getDetails();
}, [safeSetDetails, safeSetSongs]);
CodeSanbox: https://codesandbox.io/s/suspicious-dew-rsz5s?file=/src/App.js

Unexpected output using react-router-dom with React's Context API

For a small project of mine, I'm trying to implement the most basic authentication as possible, using the React context API without Redux.
import { createContext, useContext, useState } from 'react'
export const AuthContext = createContext()
export const useAuth = () => {
const context = useContext(AuthContext)
if(context === null) throw new Error('Nope')
return context
}
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(false)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
export default AuthContext
I created a context, and wrapped my <App /> component in it like so; <AuthProvider></App></AuthProvider>. Because I want to keep the authenticated state, I used the browser's local storage, for storing a simple boolean value.
import PrivateRoute from './PrivateRoute'
import { useAuth } from './context/AuthContext'
import { AuthPage } from './pages'
import {
BrowserRouter,
Switch,
Route,
} from 'react-router-dom'
import { useEffect } from 'react'
const App = () => {
const { login, authenticated } = useAuth()
useEffect(() => {
const token = localStorage.getItem('user')
if(token && token !== false) { login() }
})
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/auth" component={AuthPage} />
<Route exact path='/'>
Dashboard
</Route>
</Switch>
</BrowserRouter>
)
}
export default App
Then, in my <App /> component, I tried invoking the login callback, given from the AuthProvider, which made me assume that made me login during page refreshes. When I try to access the authenticated variable in the current component, it does work. It shows that I am authenticated.
However when I try to set up a PrivateRoute, which only authenticated users can go to like this:
import {
Route,
Redirect
} from 'react-router-dom'
import { useAuth } from './context/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { authenticated } = useAuth()
if(authenticated) {
return <Route {...rest} render={(props) => <Component {...props} />} />
}
return <Redirect to={{ pathname: '/login' }} />
}
export default PrivateRoute
It does not work. It just redirects me to the login page. How does this come? The PrivateRoute component is getting rendered from the <App /> component. Also, what would be the solution to this problem?
Rather than running a useEffect on every rerender to check if user should be logged in, you should better initialize your authenticated state with the values from your localStorage:
const storageKey = 'user'
const initialState = JSON.parse(localStorage.getItem(storageKey)) ?? false
export const AuthProvider = (props) => {
const [authenticated, setAuthenticated] = useState(initialState)
const login = () => {
setAuthenticated(true)
localStorage.setItem(storageKey, true)
}
const logout = () => {
setAuthenticated(false)
localStorage.setItem(storageKey, false)
}
return <AuthContext.Provider value={{authenticated, login, logout}} {...props}/>
}
Thanks to Yousaf for the explaination in the comments and the HospitalRun project on GitHub, I made a loading state in the <App /> component.
import { useAuth } from './context/AuthContext'
import { useEffect, useState } from 'react'
import Router from './Router'
const App = () => {
const [ loading, setLoading ] = useState(true)
const { login } = useAuth()
const token = localStorage.getItem('user')
useEffect(() => {
if(token && token !== false) {
login()
}
setLoading(false)
}, [loading, token, login])
if (loading) return null
return <Router />
}
export default App
Here I only let anything render, after the login function was called.
if (loading) return null
If this could be done any better, feedback would still be appriciated!

React-Redux: Access dispatch type in JSX

I have a login system with React-Redux and I want to display JSX when a specific dispatch Type is send.
For example when the login system failed the dispatch type is LOGIN_FAIL and I want to display an error message but when the authentication is successful then the dispatch type is LOGIN_SUCCESS and I want to display a success message. I already access the username trough mapStateToProps but I was curious if there is another way trough the dispatch type to do conditional rendering?
Thanks for your advice or tips on that topic.
This is my actions.js:
export const login = (email, password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post(`${process.env.REACT_APP_API_URL}/auth/jwt/create/`, body, config);
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
});
dispatch(load_user());
} catch (err) {
dispatch({
type: LOGIN_FAIL
});
}
};
When i do user authentication, and i want to conditially render a component. I use React routing dom. This way i can create a PrivateRoute that will render the individual component out and redirect if the user !isAuthenticated. Check this out /
https://blog.bitsrc.io/build-a-login-auth-app-with-mern-stack-part-1-c405048e3669
This is the tutorial that you can clone the git
Private Route:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { connect } from "react-redux";
import PropTypes from "prop-types";
const PrivateRoute = ({ component: Component, auth, ...rest }) => (
<Route
{...rest}
render={props =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
MAIN APP:
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import jwt_decode from "jwt-decode";
import setAuthToken from "./utils/setAuthToken";
import { setCurrentUser, logoutUser } from "./actions/authActions";
import { Provider } from "react-redux";
import store from "./store";
import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
import PrivateRoute from "./components/private-route/PrivateRoute";
import Dashboard from "./components/dashboard/Dashboard";
import "./App.css";
// Check for token to keep user logged in
if (localStorage.jwtToken) {
// Set auth token header auth
const token = localStorage.jwtToken;
setAuthToken(token);
// Decode token and get user info and exp
const decoded = jwt_decode(token);
// Set user and isAuthenticated
store.dispatch(setCurrentUser(decoded));
// Check for expired token
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
store.dispatch(logoutUser());
// Redirect to login
window.location.href = "./login";
}
}
class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<div className="App">
<Navbar />
<Route exact path="/" component={Landing} />
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
<Switch>
<PrivateRoute exact path="/dashboard" component={Dashboard} />
</Switch>
</div>
</Router>
</Provider>
);
}
}
export default App;

Categories