Hi I am trying to learn and implement react context. I have encountered a bug when tryin to update user context.
I have a login page and when a user logs in, the email address and the user name should be updated in the user context.
When I click on submit, null information is stored in the context. But, when I click on submit the second time, I can see that the context is getting updated.
authentication.component.jsx
import Navigation from "../navigation/navigation.component";
import { Button, InputGroup, Form } from "react-bootstrap";
import { UserContext, setUser, setEmail } from "../../contexts/user.context";
import { useContext, useEffect, useState } from "react";
import axios from "axios";
import React from "react";
import "./authentication.styles.scss";
const UserLogon = () => {
const { setUser, setEmail } = useContext(UserContext);
const [emailAddr, setEmailAddr] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
//console.log(emailAddr);
}, [emailAddr, password]);
const updateFormData = () => {
setEmailAddr(emailAddr);
setPassword(password);
console.log("updated");
console.log(emailAddr);
console.log(password);
};
const saveEmail = (event) => {
setEmailAddr(event.target.value);
//console.log(emailAddr);
};
const savePassword = (event) => {
setPassword(event.target.value);
//console.log(password);
};
const verifyUserHandler = (event) => {
event.preventDefault();
const baseURL = `http://localhost:4000/verify_user?email=${emailAddr}&password=${password}`;
axios
.post(baseURL)
.then((response) => {
//console.log("User verified");
if (response.data[2] === "verified") {
console.log("user verified");
var email = response.data[0];
var name = response.data[1];
console.log("email: ", email);
console.log("name: ", name);
setEmail(email);
setUser(name);
} else {
console.log("user auth error");
}
})
.catch((e) => {
console.log(e);
});
};
return (
<div className="auth-container">
<div className="auth-login">
<div className="login-info">
<Form>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
onChange={saveEmail}
/>
<Form.Text className="text-muted">
We'll never share your email with anyone else.
</Form.Text>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
onChange={savePassword}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicCheckbox">
<Form.Check type="checkbox" label="Check me out" />
</Form.Group>
<Button variant="primary" type="submit" onClick={verifyUserHandler}>
Submit
</Button>
</Form>
</div>
</div>
</div>
);
};
export default UserLogon;
user.context.jsx
import { createContext, useState, useEffect } from "react";
export const UserContext = createContext({
currentUser: null,
setCurrentUser: () => null,
userEmail: null,
setUserEmail: () => null,
});
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [userEmail, setUserEmail] = useState(null);
const setUser = (user) => {
console.log("USER: user context before", currentUser);
setCurrentUser(user);
console.log("USER: user context after", currentUser);
};
const setEmail = (email) => {
console.log("EMAIL: user context before", userEmail);
setUserEmail(email);
console.log("EMAIL: user context after", userEmail);
};
const value = { setUser, setEmail, currentUser, userEmail };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
Any help is appreciated.
thank you.
The React state setter function is async which means that at the moment you console.log it's not yet updated in your state : )
Try this to validate:
const setUser = (user) => {
setCurrentUser(user)
console.log({ currentUser, user })
// To better debug wrap vars in { }
}
const setEmail = (email) => {
setUserEmail(email)
console.log({ userEmail, email })
}
More info from docs: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Try removing the intitial values from createContext. I think it might be causing your issue.
import { createContext, useState, useEffect } from "react";
export const UserContext = createContext();
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [userEmail, setUserEmail] = useState(null);
const setUser = (user) => {
setCurrentUser(user);
console.log(currentUser);
};
const setEmail = (email) => {
setUserEmail(email);
console.log(userEmail);
};
const value = { setUser, setEmail, currentUser, userEmail };
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
Add useEffect console log to context provider to check when the state is changing.
useEffect(() => {
console.log("User", currentUser);
console.log("Email", userEmail);
}, [currentUser, userEmail]);
As you can see from here it works properly
Related
I am rying to update a 'user' from my useContext hook.
The use context checks with an axios call to see if the user is authenticated but would also handle the login and the logout.
here are my contexts and custom hooks I made to access those
authContextProvider.js
const AuthContext = React.createContext();
const LoginContext = React.createContext()
const LogoutContext = React.createContext();
const useAuth = () => {
return useContext(AuthContext);
}
const useLogin = () => {
return useContext(LoginContext)
}
const useLogout = () => {
return useContext(LogoutContext)
}
and this is my provider:
authContextProvider.js
const AuthProvider = ({children}) => {
const [auth, setAuth] = useState(false);
useEffect(() => {
axios.get("api/auth")
.then((result) => {
if(result.data.auth) {
setAuth(result.data.auth)
}
})
.catch(err => {
throw err;
})
}, [])
const login = (email, password) => {
axios.post('api/login', {
email: email,
password: password
})
.then((response) => {
console.log(response.data.auth)
setAuth(response.data.auth)
})
}
const logout = () => {
axios.get('api/logout')
.then(response => {
console.log("User has been logged out.")
setAuth(false)
})
}
return (
<AuthContext.Provider value={auth}>
<LoginContext.Provider value={login}>
<LogoutContext.Provider value={logout}>
{children}
</LogoutContext.Provider>
</LoginContext.Provider>
</AuthContext.Provider>
)
}
Problem:
In my login component, I would use the useLogin hook to trigger my API call to log in with an email and password. But I cannot seem to figure out how to pass those from my login component to my context as props.
Login.js
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// this is where my email and password would be send to my context
const handleLogin = useLogin()
return (
<>
Login
<div className={'loginForm'}>
<label htmlFor="email">Email</label>
<input type="email" id={'email'} name={'email'} onChange={(e) => {setEmail(e.target.value)}}/>
<br/>
<label htmlFor="password">Password</label>
<input type="password" id={'password'} name={'password'} onChange={(e) => {setPassword(e.target.value)}}/>
<br/>
<button onClick={() => handleLogin}>Go !</button>
</div>
<br/>
<br/>
< />
);
}
My quesion
How do I pass those props?
Or is this not intended and should this be done differently
I'm currently working on a project to implement a website to check the weather forecast.
I'm trying to get the value from the input field and when I click the submit button, this value should be set to cityName. What do I have to change in order to make this work?
import { useState, useEffect } from "react"
export function WeatherInfo() {
const token: string = '7ebe7c2a03cd48c090a193437'
async function getCurrentWeather(cityName: string): Promise<any> {
const response = await fetch(`http://api.weatherapi.com/v1/current.json?key=${token}&q=${cityName}`)
const data = await response.json()
console.log(data)
return data
}
const [cityName, setCityName]: any = useState('')
const [cityWeather, setCityWeather] = useState({})
const [value, setValue] = useState('')
const handleChange = (event: any) => {
setValue(event.target.value)
}
const handleSubmit = (event: any) => {
event.preventDefault()
setCityName(value)
}
useEffect(() => {
async function fetchData() {
const cityWeather = await getCurrentWeather(cityName)
}
fetchData()
})
return (
<div >
<form onSubmit={handleSubmit}>
<input onChange={handleChange} placeholder="Type here" />
<button>Search</button>
</form>
</div>
);
}
You should add a dependency array to your effect hook so that it triggers whenever cityName changes.
Updating the cityWeather state should only be done via the setCityWeather function.
useEffect(() => {
if (cityName) { // only fetch when you've got a value
getCurrentWeather(cityName).then(setCityWeather);
}
}, [cityName]);
You should also try to use as few any types as possible, preferably none
// define stand-alone functions outside your components
// eg weather-api.ts
const token = "your-api-key";
export interface CurrentWeather {
temp_c: number;
feelslike_c: number;
// etc
}
export async function getCurrentWeather(
cityName: string
): Promise<CurrentWeather> {
// safely encode URL query params
const params = new URLSearchParams({
key: token,
q: cityName,
});
const response = await fetch(
`http://api.weatherapi.com/v1/current.json?${params}`
);
// don't forget to check for errors
if (!response.ok) {
throw response;
}
return response.json(); // will be cast to the `CurrentWeather` type
}
import { useState, useEffect, FormEventHandler } from "react";
import { getCurrentWeather, CurrentWeather } from "./weather-api";
export function WeatherInfo() {
const [cityName, setCityName] = useState("");
const [cityWeather, setCityWeather] = useState<CurrentWeather>(); // default undefined
const [value, setValue] = useState("");
useEffect(() => {
getCurrentWeather(cityName).then(setCityWeather).catch(console.error);
}, [cityName]);
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setCityName(value);
};
return (
<div>
{cityWeather && (
<p>
The current temperature in {cityName} is {cityWeather.temp_c} °C
</p>
)}
<form onSubmit={handleSubmit}>
<input
onChange={(e) => setValue(e.target.value)}
placeholder="Type here"
/>
<button>Search</button>
</form>
</div>
);
}
This is the first time I'm asking a question here and also a newbie to coding. I'm trying to clone google classroom.
I am trying to use firestore to make a db collection when creating the class. But when I click create it doesn't create the class and create the db in firestore. It shows that the setDoc() function is invalid. Im using firestore version 9 (modular)
Here is my Form.js file. (The firestore related code is also included here)
import { DialogActions, TextField , Button} from "#material-ui/core"
import React, {useState} from 'react'
import { useLocalContext, useAuth } from '../../../context/AuthContext'
import { v4 as uuidV4 } from 'uuid'
import { db} from '../../../firebase'
import { collection, doc, setDoc } from "firebase/firestore"
const Form = () => {
const [className, setClassName] = useState('')
const [Level, setLevel] = useState('')
const [Batch, setBatch] = useState('')
const [Institute, setInstitute] = useState('')
const {setCreateClassDialog} = useLocalContext();
const {currentUser} = useAuth();
const addClass = async (e) => {
e.preventDefault()
const id = uuidV4()
// Add a new document with a generated id
const createClasses = doc(collection(db, 'CreatedClasses'));
await setDoc(createClasses, {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: id
}).then (() => {
setCreateClassDialog(false);
})
}
return (
<div className='form'>
<p className="class__title">Create Class</p>
<div className='form__inputs'>
<TextField
id="filled-basic"
label="Class Name (Required)"
className="form__input"
variant="filled"
value={className}
onChange={(e) => setClassName(e.target.value)}
/>
<TextField
id="filled-basic"
label="Level/Semester (Required)"
className="form__input"
variant="filled"
value={Level}
onChange={(e) => setLevel(e.target.value)}
/>
<TextField
id="filled-basic"
label="Batch (Required)"
className="form__input"
variant="filled"
value={Batch}
onChange={(e) => setBatch(e.target.value)}
/>
<TextField
id="filled-basic"
label="Institute Name"
className="form__input"
variant="filled"
value={Institute}
onChange={(e) => setInstitute(e.target.value)}
/>
</div>
<DialogActions>
<Button onClick={addClass} color='primary'>
Create
</Button>
</DialogActions>
</div>
)
}
export default Form
And also (I don't know whether this is helpful but my context file is below)
import React, { createContext, useContext, useEffect, useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
onAuthStateChanged,
signOut,
GoogleAuthProvider,
signInWithPopup
} from "firebase/auth";
import { auth } from "../firebase";
const AuthContext = createContext();
const AddContext = createContext()
export function useAuth() {
return useContext(AuthContext);
}
export function useLocalContext(){
return useContext(AddContext)
}
export function ContextProvider({children}){
const [createClassDialog,setCreateClassDialog] = useState(false);
const [joinClassDialog, setJoinClassDialog] = useState(false);
const value = { createClassDialog, setCreateClassDialog, joinClassDialog, setJoinClassDialog };
return <AddContext.Provider value={value}> {children} </AddContext.Provider>;
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true)
function signup(email, password) {
return createUserWithEmailAndPassword(auth,email, password);
}
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logout() {
return signOut(auth);
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email)
}
function googleSignIn() {
const googleAuthProvider = new GoogleAuthProvider();
return signInWithPopup(auth, googleAuthProvider);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged( auth, (user) => {
setCurrentUser(user);
setLoading(false)
});
return () => {
unsubscribe();
};
}, []);
return (
<AuthContext.Provider
value={{ currentUser, login, signup, logout, googleSignIn, resetPassword,updateEmail, updatePassword }}
>
{!loading && children}
</AuthContext.Provider>
);
}
The console error message:
Try something like this, excluding the collection function from setting the document.
// Add a new document with a generated id
await setDoc(doc(db, 'CreatedClasses'), {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: classId
}).then (() => {
setCreateClassDialog(false);
})
We have UserContext which sets user object which we can use throughout application. Our UserContext keep executing every time and unnecessary making api call even though dependency hasn't changed.
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Whenever we navigate to different page in our react app, we are seeing "User" route being called every time even though token isn't updated. Our token changes only when user log off.
Our AppRouter looks like following;
import React from 'react';
import AppRouter from "./AppRouter";
import { Container } from 'react-bootstrap';
import Header from './components/Header';
import { ToastProvider, DefaultToastContainer } from 'react-toast-notifications';
import 'bootstrap/dist/css/bootstrap.css';
import './scss/styles.scss';
import { UserContextProvider } from './UserContextProvider';
export default function App() {
const ToastContainer = (props) => (
<DefaultToastContainer
className="toast-container"
style={{ zIndex:100,top:50 }}
{...props}
/>
);
return (
<UserContextProvider>
<ToastProvider autoDismiss={true} autoDismissTimeout={3000} components={{ ToastContainer }}>
<Container fluid>
<Header />
<AppRouter />
</Container>
</ToastProvider>
</UserContextProvider>
)
}
This is our internal app so we want user to be logged in for 30 days and they don't have to keep login every time. So when user login first time, we create a token for them and keep that token in cookies. So if user close the browser and come back again, we check token in cookies. If token exists, we make API call to fetch user information and setUser in our context. This is the part which isn't working and it keep calling our user api during navigation to each route in application.
Here is our login.js
import React, { useState, useContext } from 'react';
import { setCookies } from '../../utils/Helper';
import APIService from '../../utils/RestApiService';
import { UserContext } from '../../UserContextProvider';
import queryString from 'query-string';
import './_login.scss';
const Login = (props) => {
const [email, setEmail] = useState(null);
const [password, setPassword] = useState(null);
const [error, setError] = useState(null);
const { siteId } = props;
const { refreshToken} = useContext(UserContext);
const onKeyPress = (e) => {
if (e.which === 13) {
attemptLogin()
}
}
let params = queryString.parse(props.location.search)
let redirectTo = "/"
if (params && params.redirect)
redirectTo = params.redirect
const attemptLogin = async () => {
const payload = {
email: email,
password: password,
siteid: siteId
};
let response = await APIService.post('/login', payload);
console.log('response - ', response)
if (response.status === 200) {
const { data } = response;
setCookies('UserToken', data.token);
refreshToken(data.token)
window.location.replace(redirectTo);
}
else {
const { error } = response.data;
setError(error);
}
}
const renderErrors = () => {
return (
<div className="text-center login-error">
{error}
</div>
)
}
return (
<div className="login-parent">
<div className="container">
<div className="login-row row justify-content-center align-items-center">
<div className="login-column">
<div className="login-box">
<form className="login-form form">
<h3 className="login-form-header text-center">Login</h3>
<div className="form-group">
<label>Email:</label>
<br/>
<input
onChange={e => setEmail(e.target.value)}
placeholder="enter email address"
type="text"
onKeyPress={onKeyPress}
className="form-control"/>
</div>
<div className="form-group">
<label>Password:</label>
<br/>
<input
onChange={e => setPassword(e.target.value)}
placeholder="enter password"
type="password"
className="form-control"/>
</div>
<div className="form-group">
<button
className="btn btn-secondary btn-block"
onClick={attemptLogin}
type="button">
Login
</button>
</div>
{error ? renderErrors() : null}
</form>
</div>
</div>
</div>
</div>
</div>
)
}
export default Login;
Our userContext looks like below
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
if (!token) return;
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Our getCookies function which simply read cookies using universal-cookies package
export const getCookies = (name) => {
return cookies.get(name);
};
So I tried to replicate your issue using a CodeSandbox, and these are my findings based on your code:
Context:
Your context has a useEffect which depend on token. When you call refreshToken, you update the token which automatically triggers the useEffect and makes a call to fetchUserInfo. So you don't need to call fetchUserInfo after setToken in refreshToken. Your context would look like:
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies("UserToken"));
const [user, setUser] = useState(null);
useEffect(() => {
console.log("Inside userContext calling as token ", token);
fetchUserInfo();
}, [token]);
const fetchUserInfo = async () => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
};
const refreshToken = (newToken) => {
setToken(newToken);
};
return (
<UserContext.Provider value={{ user, token, refreshToken }}>
{props.children}
</UserContext.Provider>
);
};
export { UserContextProvider, UserContext };
Route:
Now coming to your routing, since you've not included code of AppRouter I had to make an assumption that you use react-router with Switch component. (As shown in CodeSandbox).
I see a line in your Login component which is window.location.replace(redirectTo);. When you do this, the entire page gets refreshed (reloaded?) and React triggers a re-render, which is why I suppose your context methods fire again.
Instead use the history API from react-router (Again, my assumption) like so,
let history = useHistory();
history.push(redirectTo);
Here's the sandbox if you want to play around:
I am trying to make a simple login/register form authentication with firebase and react, but i keep getting this error when I try to load the page. "TypeError:firebase__WEBPACK_IMPORTED_MODULE_3_.default.auth is not a function"
It's a simple app that contains 3 pages. first should be the login page and then if login details are correct, that should redirect you to the HomePage.
my firebase.js file contains the usual config file. at the end i have export that file like this.
const fireDb = firebase.initializeApp(firebaseConfig);
export default fireDb.database().ref();
I am not quite sure what am I missing here, maybe just a typo? maybe I should have import it some other modules? any help will be much appreciate.
Down below my Login.js code.
Login.js
import React, { useState, useEffect } from "react";
import "./Login.css";
import { useHistory } from "react-router-dom";
import fireDb from "../firebase";
function Login() {
const history = useHistory();
const [user, setUser] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const clearInputs = () => {
setEmail("");
setPassword("");
};
const clearErrors = () => {
setEmailError("");
setPasswordError("");
};
const handleLogin = () => {
clearErrors();
fireDb
.auth()
.signInWithEmailAndPassword(email, password)
.then((auth) => {
history.push("/HomePage");
})
.catch((err) => {
// eslint-disable-next-line default-case
switch (err.code) {
case "auth/invalid-email":
case "auth/user-disabled":
case "auth/user-not-found":
setEmailError(err.message);
break;
case "auth/wrong-password":
setPasswordError(err.message);
break;
}
});
};
const handleSignup = () => {
clearErrors();
fireDb
.auth()
.createUserWithEmailAndPassword(email, password)
.then((auth) => {
if (auth) {
history.push("/HomePage");
}
})
.catch((err) => {
// eslint-disable-next-line default-case
switch (err.code) {
case "auth/email-already-in-use":
case "auth/invalid-email":
setEmailError(err.message);
break;
case "auth/weak-password":
setPasswordError(err.message);
break;
}
});
};
const authListener = () => {
fireDb.auth().onAuthStateChanged((user) => {
if (user) {
clearInputs();
setUser(user);
} else {
setUser("");
}
});
};
useEffect(() => {
authListener();
}, []);
return (
<div className="login">
<div className="login__container">
<h1>Sign-in</h1>
<form>
<h5>E-mail</h5>
<input
type="text"
autoFocus
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<p className="errorMsg">{emailError}</p>
<h5>Password</h5>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<p className="errorMsg">{passwordError}</p>
<button
type="submit"
onClick={handleLogin}
className="login__signInButton"
>
Sign In
</button>
</form>
<button onClick={handleSignup} className="login__registerButton">
Create your Account
</button>
</div>
</div>
);
}
Since you do this:
export default fireDb.database().ref();
You're effectively calling:
fireDb.database().ref().auth()
And if you check the reference documentation, you'll notice that DatabaseReference doesn't have a auth() method.
I recommend exporting the FirebaseApp object instead:
export default fireDb;
And then importing it:
import firebase from "../firebase";
And:
const fireDb = firebase.database().ref();
const fireAuth = firebase.auth();
Alternatively, you can keep your current export and import and get to FirebaseAuth with:
fireDb
.app
.auth()
.signInWithEmailAndPassword(email, password)
...
this worked for me :
import firebase from 'firebase/compat/app'
import "firebase/compat/auth";
...
onSignUp(){
const { email , password , name} = this.state;
console.log(email);
firebase.auth().createUserWithEmailAndPassword(email , password)
.then((result)=>{
console.log(result)
})
.catch((error) => {
console.log(error)
})
}