I'm trying to use the Context Api to save the user data that comes from the firebase api, but when I get these values in the component Index, it always returns the error:
TypeError: Object is not iterable (cannot read property Symbol(Symbol.iterator))
Below is my code
Where do I create the Context
import React, { createContext, useState } from "react";
const Context = createContext();
function AuthProvider({children}) {
const [userLogin, setUserLogin] = useState({});
return (
<Context.Provider value={{ userLogin, setUserLogin }} >
{ children }
</Context.Provider>
);
}
export { Context, AuthProvider }
Route File
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path='/' element={<Login />} />
<Route path='/cadastrar' element={<Register />} />
<Route path='/recuperar-senha' element={<Recovery />} />
<Route path='/anuncios' exact element={<Index />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
Here where I set the context with firebase data
const {setUserLogin} = useContext(Context);
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = data => {
const auth = getAuth();
signInWithEmailAndPassword(auth, data.email, data.password)
.then((userCredential) => {
const user = userCredential.user;
setUserLogin({user});
navigate('/anuncios');
})
.catch((error) => {
let erroMessage = localizeErrorMap(error.code)
Toast('error', erroMessage);
})
};
I want to save the data in the context api and be able to take, for example, the component below and the others
import React, {useContext} from "react";
import { Context } from '../providers/auth';
export default function Index() {
const [user, setUserLogin] = useContext(Context);
//console.log(user);
return (
<h1>Logado {user}</h1>
)
}
I saw that in the browser console when I click on the component, it informs that the problem is in the line: const [user, setUserLogin] = useContext(Context);
Your context value is { userLogin, setUserLogin }:
<Context.Provider value={{ userLogin, setUserLogin }}>
so you cannot destructure it into an array. Use const { userLogin, setUserLogin } = useContext(Context) instead:
export default function Index() {
const { userLogin, setUserLogin } = useContext(Context);
console.log(userLogin);
...
Related
I am making a site whereby after the user signs in, the user is meant to be redirected to the home page. The homepage and all the other pages of the site are only accessible by signed in users but even after a user signs in(firebase auth), the rest of the site(protected routes) is still not accessible and the only page accessible is the login page. The technologies I am using are react, react router dom and firebase and this is how my code looks like, starting with the App.js
import Home from "./pages/home/Home";
import Login from "./pages/login/Login";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import List from "./pages/list/List";
import User from "./pages/user/User";
import AddNew from "./pages/addnew/AddNew";
import { useContext } from "react";
import { AuthContext } from "./context/AuthContext";
function App() {
const {currentUser} = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path="/login" exact element={<Login />} />
<Route path="/" exact element={ <RequireAuth> <Home /> </RequireAuth> } />
<Route path="/users" exact element={<RequireAuth><List /></RequireAuth>} />
<Route path="/users/:id" exact element={<RequireAuth><User /></RequireAuth>} />
<Route path="/add" exact element={<RequireAuth><AddNew /></RequireAuth>} />
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
And then followed by the login.js page
import React,{useState} from 'react'
import { useContext } from 'react';
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from '../../firebase';
import "./login.css";
import {useNavigate} from "react-router-dom";
import { AuthContext } from '../../context/AuthContext';
export default function Login() {
const [error, seterror] = useState(false);
const [email, setemail] = useState("");
const [password, setpassword] = useState("");
const navigate = useNavigate();
const {dispatch} = useContext(AuthContext)
const handleLogin = (e) => {
e.preventDefault();
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
dispatch({type: "LOGIN", payload: user});
navigate("/");
})
.catch((error) => {
seterror(true);
console.log(error.message);
});
}
return (
<div className='login'>
<form onSubmit={handleLogin}>
<input className='ok' type="email" placeholder='email' onChange={e => setemail(e.target.value)} />
<input className='ok' type="password" placeholder='password' onChange={e => setpassword(e.target.value)} />
<button className='sb'>Submit</button>
{error && <span className='ks'>Wrong email or password</span>}
</form>
</div>
)
}
And then I have the authreducer.js file that deals with the state
const AuthReducer = (state, action) => {
switch (action.type) {
case "LOGIN": {
return {
currentUser: action.payload,
}
}
case "LOGOUT": {
return {
currentUser: null
}
}
default:
return state;
}
}
export default AuthReducer
And finally the authcontext.js file
import { createContext, useEffect, useReducer } from "react";
import AuthReducer from "./AuthReducer";
const INITIAL_STATE = {
currentUser: JSON.parse(localStorage.getItem("user")) || null,
}
export const AuthContext = createContext(INITIAL_STATE);
export const AuthContextProvider = ({children}) => {
const [state, dispatch] = useReducer(AuthReducer, INITIAL_STATE);
useEffect(() => {
localStorage.setItem("user", JSON.stringify(state.currentUser))
}, [state.currentUser])
return (
<AuthContext.Provider value={{current: state.current, dispatch}}>
{children}
</AuthContext.Provider>
)
}
I do not know what could be causing this problem but I have an idea that it has something to do with the state because it was redirecting well before I started combining it with the state. What could be the problem
Issue
From that I can see, the App isn't destructuring the correct context value to handle the conditional route protection.
The AuthContextProvider provides a context value with current and dispatch properties
<AuthContext.Provider value={{ current: state.current, dispatch }}>
{children}
</AuthContext.Provider>
but App is accessing a currentUser property, which is going to be undefined because state.current is undefined.
const { currentUser } = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
The Navigate component will always be rendered.
Solution
Assuming the handleLogin handler correctly updates the state then the solution is to be consistent with state properties.
<AuthContext.Provider value={{ currentUser: state.currentUser, dispatch }}>
{children}
</AuthContext.Provider>
...
const { currentUser } = useContext(AuthContext);
const RequireAuth = ({ children }) => {
return currentUser ? children : <Navigate to="/login" />;
};
I don't know why, changing the props state inside useEffect causes infinite loop of errors. I used them first locally declaring within the function without using props which was running ok.
EDIT:
Home.js
import Axios from "axios";
import React, { useEffect, useState } from "react";
function Home(props) {
// const [details, setDetails] = useState({});
// const [login, setLogin] = useState(false);
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
props.setLogin(true);
props.setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return props.login ? (
<div>
<div>Welcome {props.setUserdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
}
export default Home;
App.js
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setLogin={setLogin}
login={login}
setUserdetails={setUserdetails}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
Here I initialized the states directly in App.js so I don't have to declare it on every page for the route renders. I just passed them as props to every component.
I suggest to create a componente Home with the post and two sub-component inside:
const Home = () => {
const [userDetails, setUserDetails] = useState({});
const [login, setLogin] = useState(false);
useEffect(() => {
// api call
}, []);
return (
<>
<Welcome login={login} details={userDetails} />
<Bars login={login} details={userDetails} />
</>
);
};
where Welcome is the following:
const Welcome = ({ userdetails, login }) => (
<>
login ? (
<div>
<div>Welcome {userdetails.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
</>
);
A better solution is to use only one state variable:
const [userDetails, setUserDetails] = useState(null);
and test if userDetails is null as you test login is true.
An alternative if you have to maintain the call as you write before, you can use two state as the follow:
function App() {
const [userdetails, setUserdetails] = useState(null);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
setUserdetails={setUserdetails}
/>
<Bars login={!!userdetails} />
</>
}
/>
<Routes>
<Router>
);
and on Home component use a local state:
const Home = ({setUserdetails}) => {
const [userDetailsLocal, setUserDetailsLocal] = useState(null);
useEffect(() => {
// api call
// ... on response received:
setUserdetails(res.data[0]);
setUserDetailsLocal(res.data[0]);
// ...
}, []);
userDetailsLocal ? (
<div>
<div>Welcome {userDetailsLocal.FullName}</div>
</div>
) : (
<div>You need to login first</div>
);
};
I advise to follow Max arquitecture for your solution. the problem lies in the Router behavior. React Router is not part of React core, so you must use it outside your react logic.
from documentation of React Router:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render.
https://v5.reactrouter.com/web/api/Route/component
Edit:
ok, you make me write it. A solution could be like:
function App() {
const [login, setLogin] = useState(false);
const [userdetails, setUserdetails] = useState({});
useEffect(() => {
try {
const data = localStorage.getItem("expensesAccDetails");
if (data) {
Axios.post("http://localhost:3001/eachCollectionData", {
collection: data,
}).then((res) => {
if (res.data.err) {
console.log("Error");
} else {
console.log(res.data[0]);
setLogin(true);
setUserdetails(res.data[0]);
}
});
}
} catch (err) {
console.log(err);
}
}, []);
return (
<Router>
<Routes>
<Route
path="/Home"
element={
<>
<Home
login={login}
userdetails={userdetails}
/>
<Bars login={login} />
</>
}
/>
<Routes>
<Router>
);
I am making a simple SPA where you need to login before you can access other pages. I can successfully login and store the login data (firstname, lastname, etc.) cause I plan to use the data again later in the other pages. The problem is whenever I refresh the page, it always empty the state in the context which cause me to return to the login page. I am referring link for my SPA.
Do I need to do this? I would be thankful if someone can point out what I should change / improve. Thank you.
Here is my code.
App.js
import React, { useState } from "react";
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
import { AuthContext } from "./context/auth";
import PrivateRoute from "./PrivateRoute";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import Home from "./pages/Home";
import Admin from "./pages/Admin";
function App() {
const [authTokens, setAuthTokens] = useState();
const setTokens = (data) => {
// console.log("DATA ",data);
localStorage.setItem("tokens", JSON.stringify(data));
setAuthTokens(data);
}
// console.log(authTokens);
return (
<AuthContext.Provider value={{ authTokens, setAuthTokens: setTokens }}>
<Router>
<div className="app">
<ul>
<li><Link to="/">Home Page</Link></li>
<li><Link to="/admin">Admin Page</Link></li>
</ul>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/" component={Home} />
<PrivateRoute exact path="/admin" component={Admin} />
</div>
</Router>
</AuthContext.Provider>
);
}
export default App;
Login.js
import React, { useState } from "react";
import axios from "axios";
import { Link, Redirect } from "react-router-dom";
import { useAuth } from "../context/auth";
import { Card, Form, Input, Button, Error } from "../components/AuthForm";
const Login = () => {
const [isLoggedIn, setLoggedIn] = useState(false);
const [isError, setIsError] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const { setAuthTokens } = useAuth();
const handleLogin = () => {
axios
.post("LOGINLINK", {
email,
password,
})
.then((result) => {
if (result.status === 200) {
setAuthTokens(result.data);
setLoggedIn(true);
} else {
setIsError(true);
}
})
.catch((error) => {
setIsError(true);
});
};
if (isLoggedIn) {
return <Redirect to="/" />;
}
return (
<Card>
<Form>
<Input
type="email"
placeholder="Email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
/>
<Input
type="password"
placeholder="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<Button onClick={handleLogin}>Login</Button>
</Form>
<Link to="/signup">Don't have an account?</Link>
{isError && (
<Error>The username or password provided were incorrect!</Error>
)}
</Card>
);
};
export default Login;
Auth.js
import { createContext, useContext } from "react";
export const AuthContext = createContext();
export function useAuth() {
console.log("CONTEXT", useContext(AuthContext));
return useContext(AuthContext);
}
In your App component you need to fetch the data from localStorage when initializing your state so it has some data to start with.
const localToken = JSON.parse(localStorage.getItem("tokens"));
const [authTokens, setAuthTokens] = useState(localToken);
If user has already authenticated it will be available in localStorage else it's going to be null.
I also had same problem but I solved liked this Don't use localStorage directly use your state and if it is undefined then only use localStorage. cause directly manipulating state with localStorage is in contrast with react internal state and effects re-render .
const getToken = () => {
JSON.parse(localStorage.getItem('yourtoken') || '')
}
const setToken = (token) => {
localStorage.setItem('key' , token)
}
const [authTokens, setAuthTokens] = useState(getToken());
const setTokens = (data) => {
// console.log("DATA ",data);
setToken(token);
setAuthTokens(data);
}
I'm using React contexts in order to hold my authentication state for my application. Currently, I'm having an issue where whenever I try and hit /groups/:id, it always redirects me to /login first and then to /UserDash. This is happening because the context of my AuthProvider isn't updating fast enough, and my Private Route utilized the AuthContext to decide whether to redirect or not.
<AuthProvider>
<Router>
<Switch>
<LoggedRoute exact path = "/" component = {Home}/>
<Route exact path = "/login" component = {Login}/>
<PrivateRoute exact path = "/groups/:id" component = {GroupDash}/>
<PrivateRoute exact path = "/UserDash" component = {UserDash}/>
</Switch>
</Router>
</AuthProvider>
In another file:
export const AuthContext = React.createContext();
export const AuthProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
firebaseApp.auth().onAuthStateChanged(setCurrentUser);
},[]);
return (
<AuthContext.Provider
value = {{currentUser}}
>
{children}
</AuthContext.Provider>
);
My private route:
const PrivateRoute = ({component: RouteComponent, ...rest}) => {
const {currentUser} = useContext((context) => AuthContext);
return (
<Route
{...rest}
render={routeProps => (currentUser) ? (<RouteComponent{...routeProps}/>) : (<Redirect to={"/login"}/>)}
/>
)
};
And my login page
const {currentUser} = useContext(AuthContext);
if (currentUser) {
return <Redirect to = "/UserDash" />
}
return (
<button onClick={(e) => {googleLogin(history)}}>Log In</button>
);
Is there any way to force the context to load before the private route redirects the user to the login page? I've tried using firebase.auth().currentUser inside my PrivateRoute instead, but that also fails and I still get redirected to "/login", before getting pushed to "/UserDash".
Since useEffect runs after a render and the value being fetched in useEffect is from a async call what can do is to actually maintain a loading state till data is available like
export const AuthContext = React.createContext();
export const AuthProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
firebaseApp.auth().onAuthStateChanged((user) => {
setCurrentUser(); setIsLoading(false)
});
},[]);
return (
<AuthContext.Provider
value = {{currentUser, isLoading}}
>
{children}
</AuthContext.Provider>
);
}
Then in privateRoute
const PrivateRoute = ({component: RouteComponent, ...rest}) => {
const {currentUser, isLoading} = useContext((context) => AuthContext);
if(isLoading) return <div>Loading...</div>
return (
<Route
{...rest}
render={routeProps => (currentUser) ? (<RouteComponent{...routeProps}/>) : (<Redirect to={"/login"}/>)}
/>
)
};
I'm beginning to learn React Js. I was trying to create a auth with hooks.
But I recived en error:
Unhandled Rejection (Error): Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside
componentWillUpdate or componentDidUpdate. React limits the number of
nested updates to prevent infinite loops.
This is my code I tried to simplify the components, I hope it's clear
export const AuthContext = React.createContext();
export const AuthProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null);
useEffect( () => {
//const token = localStorage.getItem( 'token' );
//const userName = localStorage.getItem( 'userName' );
console.log('useEffect Auth Provider');
console.log(currentUser);
}, [] );
return (
<AuthContext.Provider
value={
[currentUser, setCurrentUser]
}
>
{children}
</AuthContext.Provider>
);
}
When I try to login in Login.js :
export const Login = () => {
const [ currentUser, setCurrentUser ] = useContext( AuthContext );
// Login
const handleLogin = (event) => {
event.preventDefault();
const { email, password } = event.target.elements;
console.log(email.value, password.value);
const siteUrl = clientConfig.serverUrl;
const loginData = {
email: email.value,
password: password.value
};
axios.post( `${siteUrl}/api/users/login`, loginData )
.then( res => {
setCurrentUser(res.data);
console.log(res.data);
});
}
if (currentUser) {
return <Redirect to="/" />
}
else {
return (
<form onSubmit={handleLogin}>
<input name="email" type="text" placeholder="E-Mail"></input>
<input name="password" type="password" placeholder="**************"></input>
<button type="submit">Login</button>
</form>
);
}
};
App.js:
function App() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<Route exact path="/login" component={Login} />
<Route path="*" component={NotFound} />
</Switch>
</Router>
</AuthProvider>
);
}
export default App;
// PrivateRoute
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../context/auth";
export const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const {currentUser} = useContext(AuthContext);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
Where am I wrong? Thanks to anybody who want to help me.
Marco.Italy
Here's a working version of your code:
https://codesandbox.io/s/focused-dubinsky-yhpcl
The problem was in the way you were accessing your current user on your PrivateRoute. It was coming back as undefined.
const { currentUser } = useContext(AuthContext);
You can't destructure an array like that. So I changed to this:
const [currentUser, setCurrentUser] = useContext(AuthContext);
NOTE: I know you don't need the setCurrentUser on PrivateRoute. But it's just a way to make it work clearly as is. You can also do it like this:
const [currentUser] = useContext(AuthContext); // THIS WORKS WHEN YOU'RE GETTING THE FIRST ARRAY VALUE
PrivateRoute.js
export const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
console.log("Rendering PrivateRoute...");
const [currentUser, setCurrentUser] = useContext(AuthContext); // <-------------
console.log("currentUser: " + currentUser);
return (
<Route
{...rest}
render={routeProps =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};