Changing the state of a context within an if-else in JavaScript - javascript

I tried making a global state by using a Context and have the following for my AuthProvider.js file
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
I want to be able to change its boolean state within an if-else instead of a button with an onCLick event in the tutorial but it shows an error in the console: setAuth is not a function
function Home() {
const setAuth = useContext(AuthProvider);
const [usernameReg, setUsernameReg] = useState("");
const [passwordReg, setPasswordReg] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loginStatus, setloginStatus] = useState("");
const uLogin = document.getElementById('userLogin');
const pLogin = document.getElementById('passwordLogin');
const register = () => {
if (usernameReg === "" || passwordReg === ""){
alert("Fields can't be blank");
return;
}
Axios.post('http://localhost:3001/register', {
username: usernameReg,
password: passwordReg,
}).then((response) => {
console.log(response);
});
}
const login = () => {
Axios.post('http://localhost:3001/login', {
username: username,
password: password,
}).then((response) => {
if (response.data.message){
setloginStatus(response.data.message);
alert(response.data.message);
}
else{
setloginStatus(response.data[0].username);
setAuth(true); <====================================== here
window.location.href = "/map";
}
console.log(response.data);
});
}
return (
<div className="Home">
<h1>Register</h1>
<label>Username: </label>
<input type="text"
onChange={(e) => {
setUsernameReg(e.target.value)
}}/>
<label><p></p>Password: </label>
<input type="text"
onChange={(e)=> {
setPasswordReg(e.target.value)
}}/>
<p></p>
<button onClick={register}> Register </button>
<h1>--------------------------------------------------------</h1>
{/*<form id="loginForm">*/}
<h1>Log In</h1>
<input id="userLogin" type="text" placeholder="Username"
onChange={(e) => {
setUsername(e.target.value)
}}
/>
<p></p>
<input id="passwordLogin" type="password" placeholder="Password"
onChange={(e) => {
setPassword(e.target.value)
}}
/>
<p></p>
<button onClick={login}> Log In </button>
{/*</form>*/}
<h1>{loginStatus}</h1>
</div>
)
}
my App.js is below
import React, { useState } from "react";
import { BrowserRouter,Route, Routes} from 'react-router-dom';
import './App.css';
import Home from "./Pages/Home";
import MapPage from "./Pages/MapPage.js";
import ProtectedRoutes from "./ProtectedRoutes";
import { AuthProvider } from "./components/AuthProvider"
function App() {
const [auth, setAuth ] = useState(false);
//const [auth, setAuth] = useState(false);
return (
<BrowserRouter>
<Routes element ={<AuthProvider />} value={{auth, setAuth}}>
<Route element ={<ProtectedRoutes />}>
<Route element={<MapPage />} path="/map" exact/>
</Route>
<Route path="/" element={<Home />}/>
</Routes>
</BrowserRouter>
);
}
export default App;
index.js below
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AuthProvider } from './components/AuthProvider'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthProvider>
<App />
</AuthProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bitly/CRA-vitals
reportWebVitals();
I'm very new to JavaScript and I've only been following along tutorials for the functions or features I need for the project. Is there a way for this?
I'm also guessing that the code for the context file is overkill for a simple Boolean state.

You probably want to check the context docs again, and try to understand how and why you set them up, and how to access them. You tried accessing the context by passing the provider as a param to the useContext hook. This is wrong, you need to pass the Context itself. You also called the return from the context setAuth, but the return is the context itself. I urge you to read the documentation thourougly. However, this is probably what you wanted:
type AuthContextType = {
auth: boolean;
setAuth: Dispatch<SetStateAction<boolean>>;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
const [auth, setAuth] = useState(false);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
};
const useAuthContext = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("No AuthContext Provider found");
return ctx // as AuthContextType<...> if you need the context to be generic and to infer generics.
};
function Home() {
const { auth, setAuth } = useAuthContext();
...
}

Related

My code throwing "Rendering<Context>directly is not supported" and "A context consumer was rendered with multiple children" errors

I am trying to make a chat website, and i was following along to this YouTube tutoriel and I came across these two errors:
"Renderingdirectly" is not supported and "A context consumer was rendered with multiple children"
Here is my App code:
import React from 'react';
import ApolloProvider from './ApolloProvider';
import {Container} from 'react-bootstrap'
import Register from './pages/register'
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom'
import './App.scss';
import Login from './pages/Login';
import Home from './pages/Home'
import {AuthProvider} from './context/auth';
function App() {
return (
<ApolloProvider >
<AuthProvider>
<Router>
<Container className='pt-5'>
<Routes>
<Route exact path='/' element={<Home />}/>
<Route path='/register' element={<Register />}/>
<Route path='/login' element={<Login />}/>
</Routes>
</Container>
</Router>
</AuthProvider>
</ApolloProvider>
);
}
export default App;
And here is my context code (
I am setting the token in my LOGIN case, which is being accessed from my login.js):
import React, {createContext, useReducer, useContext} from 'react'
const AuthStateContext = createContext()
const AuthDispatchContext = createContext()
const authReducer = (state, action) =>{
switch(action.type){
case 'LOGIN':
localStorage.setItem("token", action.payload.token)
return {
...state,
user: action.payload,
}
case 'LOGOUT':
return {
...state,
user: null
}
default:
throw new Error(`Unknown action type: ${action.type}`)
}
}
export const AuthProvider = (props) => {
const [state, dispatch] = useReducer(authReducer, {user: null})
return (
<AuthDispatchContext.Provider value={dispatch}>
<AuthStateContext value={state}>
{props.children}
</AuthStateContext>
</AuthDispatchContext.Provider>
)
}
export const useAuthState = () => useContext(AuthStateContext)
export const useAuthDispatch = () => useContext(AuthDispatchContext)
Here is a snippet of my login code where we pass the token to the context file:
import { useAuthDispatch } from '../context/auth';
const LOGIN_USER = gql`
query login(
$username: String!
$password: String!
) {
login(
username: $username
password: $password
) {
username email createdAt token
}
}
`;
export default function Login() {
const [variables, setVariables]= useState({
username: '',
password: ''
})
const Navigate = useNavigate()
const [errors,setErrors] = useState({})
const dispatch = useAuthDispatch()
const [loginUser, {loading}] = useLazyQuery(LOGIN_USER,{
onError: (err)=>{
setErrors(err.graphQLErrors[0].extensions.errors)
},
onCompleted: (data)=>{
dispatch({type:'LOGIN', payload: data.login})
Navigate('/')
}
})
const submitLogin = (e) =>{
e.preventDefault()
loginUser({variables})
}
You are rendering AuthStateContext while it should be AuthStateContext.Provider, like so:
export const AuthProvider = (props) => {
const [state, dispatch] = useReducer(authReducer, { user: null });
return (
<AuthDispatchContext.Provider value={dispatch}>
<AuthStateContext.Provider value={state}>{props.children}</AuthStateContext.Provider>
</AuthDispatchContext.Provider>
);
};

setAuth is not a function at handleSubmit

hello I''m trying to use Auth in login page for chicking if the user is logged in or not, the front end keep sending "getAuth is not a function at handleSubmit", the backEnd working as it supposed to.
login.js
import { useRef, useState, useEffect } from 'react';
import axios from 'axios'
import UseAuth from '../hooks/useAuth';
import {Link , useNavigate , useLocation} from 'react-router-dom'
const Login = () => {
const {setAuth}= UseAuth()
const navigate = useNavigate()
const location = useLocation()
// console.log(location.state.from.pathname)
const from = location.state?.from?.pathname || '/';
const userRef = useRef();
const errRef = useRef();
const [user, setUser] = useState('');
const [pwd, setPwd] = useState('');
const [errMsg, setErrMsg] = useState('');
useEffect(() => {
userRef.current.focus();
}, [])
useEffect(() => {
setErrMsg('');
}, [user, pwd])
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:4000/login',
JSON.stringify({ user, pwd }),
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
);
const accessToken = response?.data?.accessToken;
const roles = response?.data?.roles;
setAuth({ user, pwd, roles, accessToken });
setUser('');
setPwd('');
navigate(from, {replace:true})
} catch (err) {
console.log(err)
if (!err?.response) {
setErrMsg('No Server Response');
} else if (err.response?.status === 400) {
setErrMsg('Missing Username or Password');
} else if (err.response?.status === 401) {
setErrMsg('Unauthorized');
} else {
setErrMsg('Login Failed');
}
errRef.current.focus();
}
}
return (
<>
<section>
<p ref={errRef} className={errMsg ? "errmsg" : "offscreen"} aria-live="assertive">{errMsg}</p>
<h1>Sign In</h1>
<form onSubmit={handleSubmit}>
<label htmlFor="username">Username:</label>
<input
type="text"
id="username"
ref={userRef}
autoComplete="off"
onChange={(e) => setUser(e.target.value)}
value={user}
required
/>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
onChange={(e) => setPwd(e.target.value)}
value={pwd}
required
/>
<button>Sign In</button>
</form>
<p>
Need an Account?<br />
<Link to ={'/register'} className="line">
{/*put router link here*/}
Sign Up
</Link>
</p>
</section>
</>
)
}
export default Login
authProvider.js
this function is to store the data so I can use it later
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth , setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
useAuth.js hook
I use this function in other pages to pass the data to the authProvider.js
import { useContext } from "react";
import AuthContext from "../context/authProvider";
const UseAuth = () => {
return useContext(AuthContext);
}
export default UseAuth;
I'm guessing you need to destructure your object in Login
const { setAuth } = UseAuth()
Because your hook is providing an object that contains both auth and setAuth
I've resolved the issue the only thing that I had to do is destructure the UseAuth component: setAuth.setAuth({ user, pwd, roles, accessToken })
Because I'm storing the auth and the setAuth method in UseAuth which saved in setAuth variable
The function is not recognised probably because you have not wrapped the application with the AuthProvider context in index.js!
Eg:
import { AuthProvider } from './services/AuthProvider';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Router>
<AuthProvider>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</AuthProvider>
</Router>
);

Problem with React Router v6 - Showing a blank page

I'm trying to figure out how to use react-router for applications with react. But my route part doesn't work, it shows a blank page in any case.
Here are the code parts of the files used:
index.js
import ReactDOM from "react-dom"
import App from './components/App';
import 'bootstrap/dist/css/bootstrap.min.css'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
App.js
import { Container } from "react-bootstrap";
import Signup from "./Signup";
import HomePage from "./HomePage";
import { AuthProvider } from "../context/AuthContext"
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
function App() {
return (
<Container className="d-flex aling-items-center justify-content-center" style={{ minHeight: "100vh" }}>
<div className="w-100" style={{ maxWidth: "400px", display: "flex", flexDirection: "column", justifyContent: "center"}}>
<Router>
<AuthProvider>
<Routes>
<Route path="/" element={<HomePage />}></Route>
<Route path="/signup" element={<Signup />}></Route>
</Routes>
</AuthProvider>
</Router>
</div>
</Container>
)
}
export default App;
Home.js and Signup.js
import React from 'react'
export default function HomePage() {
return (
<div>This is the homepage</div>
)
}
AuthContext.js and firebase.js
import React, { useContext, useState, useEffect } from 'react'
import { auth } from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({children}) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState('true')
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading('false')
})
return unsubscribe
}, [])
const value = {
currentUser,
signup
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
//------------------(firebase.js)---------------------
import firebase from 'firebase/compat/app'
import "firebase/compat/auth"
const app = firebase.initializeApp({
apiKey: "***************************************",
authDomain: process.env.REACT_AUTH_DOMAIN,
projectId: process.env.REACT_PROJECT_ID,
storageBucket: process.env.REACT_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_ID
})
export const auth = app.auth()
export default app
I had to specify the key as a string because writing it as the other parameters didn't work
I've also tried using different versions of react-router, reading other answered questions, or writing the code differently. Nothing worked
Don't use strings "true" and "false" in lieu of actual boolean values. Non-empty strings are always truthy. Change your loading state from strings to booleans.
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
function signup(email, password) {
return auth.createUserWithEmailAndPassword(email, password);
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, [])
const value = {
currentUser,
signup
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}

Update Context state before component loads

Here I am using react-router and using conditionals for multiple routes. I am using context to store my state. I want to load my data from localStorage to the state before my App.js is loaded as based on that it tells on which routes should I navigate to.
userContext.js
import React, { useState, createContext, useEffect } from "react";
export const userContext = createContext();
export const UserProvider = (props) => {
const [user, setUser] = useState(null);
const loadFromLocalStorage = () => {
try {
const serializedState = localStorage.getItem("state");
if (serializedState == null) return undefined;
return JSON.parse(serializedState);
} catch (err) {
console.log(err);
return null;
}
};
useEffect(() => {
const state = loadFromLocalStorage();
console.log("state: ", state);
setUser(state);
}, []);
return (
<userContext.Provider value={[user, setUser]}>
{props.children}
</userContext.Provider>
);
};
export default userContext;
App.js
import React, { useContext, useEffect } from "react";
import { Switch, Route, Redirect } from "react-router";
import userContext from "./context/userContext";
const App = () => {
var routes = null;
const [user, setUser] = useContext(userContext);
console.log("app: ", user);
console.log(routes);
if (user == null) {
routes = (
<div>
<Switch>
<Route path="/login" component={SignUp} />
<Redirect to="/login"></Redirect>
</Switch>
</div>
);
} else {
routes = (
<div>
<Switch>
<Route exact path="/news" component={NewsList} />
<Redirect to="/news"></Redirect>
</Switch>
</div>
);
}
return <div className="App">{routes}</div>;
};
export default App;
Inside your UserProvider, don't render the children until you've loaded data from localstorage. This will require some extra state, but nothing too bad, and this is a common pattern for asynchronous context/state:
export const UserProvider = (props) => {
const [isReady, setIsReady] = useState(false);
// other code removed for brevity
useEffect(() => {
const state = loadFromLocalStorage();
setIsReady(true);
}, [])
return (
<userContext.Provider value={[user, setUser]}>
{isReady ? props.children : null}
</userContext.Provider>
);
}

Infinite loop when working with react and react-firebase-hooks

I am working with a navigation bar that should be able to switch between multiple chat rooms using react and react-firebase-hooks. (https://github.com/CSFrequency/react-firebase-hooks)
However, the chat room will infinitely re-render itself when I choose a room in nav-bar.
I initially thought this is a router issue, but having each rooms sharing the same url, the issue persists.
Right now, when I choose a room using the nav bar, it will send that room number back to App.js using a callback function. App.js will pass that room number to ChatRoom.js, which will get the data from firestore, and re-render itself.
I struggled for several days trying to find anything that could cause the infinite loop with minimal success. Any help would be appreciated!
ChatRoom.js
import React, { useMemo, useRef, useState } from 'react';
import { withRouter } from 'react-router';
import { useCollectionData, useDocument, useDocumentData } from 'react-firebase-hooks/firestore';
import firebase, { firestore, auth } from '../Firebase.js';
import ChatMessage from './ChatMessage';
const ChatRoom2 = (props) => {
console.log("chat room rendered");
function saveQuery(){
const channelid= props.channelid;
const messagesRef = firestore.collection('messages').doc(channelid).collection('chats');
const query = messagesRef.orderBy('createdAt').limitToLast(25);
return [messagesRef,query];
}
var returnedVal = useMemo(()=>saveQuery , [props.channelid]);
const messagesRef = returnedVal[0];
const query = returnedVal[1];
const [messages] = useCollectionData(query, { idField: 'id' });
const [formValue, setFormValue] = useState('');
const sendMessage = async (e) => {
e.preventDefault();
console.log(messagesRef);
console.log(query);
console.log(messages);
const { uid, photoURL } = auth.currentUser;
await messagesRef.add({
text: formValue,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
uid,
photoURL
})
setFormValue('');
}
return (<>
<main>
{messages && messages.map(msg => <ChatMessage key={msg.id} message={msg} />)}
</main>
<form onSubmit={sendMessage}>
<input value={formValue} onChange={(e) => setFormValue(e.target.value)} placeholder="say something nice" />
<button type="submit" disabled={!formValue}>🕊️</button>
</form>
</>)
}
export default ChatRoom2;
ChatList.js (nav bar)
const ChatList = (props) => {
console.log("list rendered");
const query = firestore.collection('users').doc(auth.currentUser.uid).collection('strangers').orderBy('channelID').limitToLast(10);
//console.log(query);
const [channelidArr] = useCollectionData(query, { idField: 'id' });
return (
<div>
{channelidArr && channelidArr.map(channelid =>
<div>
<button onClick={() => props.parentCallback(channelid.channelID)}>{channelid.channelID}</button>
<br />
</div>)}
</div>
);
};
export default ChatList;
App.js
import React, { useRef, useState } from 'react';
import {
BrowserRouter,
Switch,
Route,
Link
} from "react-router-dom";
//import './App.css';
import firebase, { firestore, auth } from './Firebase.js';
import { useAuthState } from 'react-firebase-hooks/auth';
import { useCollectionData } from 'react-firebase-hooks/firestore';
import ChatList from './components/ChatList.js';
import FindNew from './components/FindNew.js';
import Footer from './components/Footer.js';
import Profile from './components/Profile.js';
import ChatRoom2 from './components/ChatRoom2.js';
import SignOut from './components/SignOut.js';
import SignIn from './components/SignIn.js';
import SignUp from './components/SignUp.js';
import ChatRoom from './components/ChatRoom.js';
function App() {
console.log('App rendered');
const [user] = useAuthState(auth);
const [roomNum, setRoomNum] = useState([]);
const callbackFunction = (childData) => {
setRoomNum(childData);
};
return (
<div className="App">
<header>
<h1>⚛️🔥💬</h1>
<SignOut auth={auth} />
</header>
<BrowserRouter >
<Footer />
<Switch>
<Route path="/profile">
<Profile />
</Route>
<Route path="/new">
<FindNew />
</Route>
<Route path="/signup">
{() => {
if (!user) {
return <SignUp />;
} else {
return null;
}
}}
</Route>
<Route path="/direct">
{user ?
<div>
<ChatList parentCallback={callbackFunction} />
<ChatRoom2 channelid={roomNum} />
</div> : <SignIn />}
</Route>
</Switch>
</BrowserRouter>
</div>
);
};
export default App;
Issue Summary
useCollectionData memoizes on the query parameter but since a new query reference was declared each render cycle the firebase hook was rerun and updated collection and rerendered the component.
const { channelid } = props;
const messagesRef = firestore
.collection('messages')
.doc(channelid)
.collection('chats');
const query = messagesRef // <-- new query reference
.orderBy('createdAt')
.limitToLast(25);
const [messages] = useCollectionData(
query, // <-- reference update trigger hook
{ idField: 'id' },
);
Solution
query has only a dependency on the channelid prop value, so we can memoize the query value and pass a stable value reference to the useCollectionData hook.
const { channelid } = props;
const query = useMemo(() => {
const messagesRef = firestore
.collection('messages')
.doc(channelid)
.collection('chats');
const query = messagesRef.orderBy('createdAt').limitToLast(25);
return query;
}, [channelid]);
const [messages] = useCollectionData(
query, // <-- stable reference
{ idField: 'id' },
);

Categories

Resources