React context api update state instantly - javascript

I'm using the context api in a Gatsby setup to keep track of a state called userIsLoggedIn. I'm using Firebase for authentication.
This is my context file:
import { createContext } from "react"
export const AppContext = createContext(null)
This is my AppWrapper component:
import React, { useState, useEffect } from "react"
import firebase from "../../config/firebase"
import { AppContext } from "../../context/AppContext"
const AppWrapper = ({ children }: any) => {
const [userIsLoggedIn, setUserIsLoggedIn] = useState(false)
const authListener = () => {
firebase.auth().onAuthStateChanged(user => {
if (user && user.emailVerified) {
setUserIsLoggedIn(true)
} else {
setUserIsLoggedIn(false)
}
})
}
useEffect(() => {
authListener()
}, [])
return (
<>
<AppContext.Provider
value={{
userIsLoggedIn,
}}
>
<main>{children}</main>
</AppContext.Provider>
</>
)
}
export default AppWrapper
This is my index page where I want to keep track if the user is logged in so I can show/hide certain content:
import React, { useContext } from "react"
import { AppContext } from "../context/AppContext"
const IndexPage = () => {
const app = useContext(AppContext)
console.log("app", app)
return (
<>
{app && app.userIsLoggedIn && (
<>
<h1>Hello dearest user</h1>
<p>Welcome to your page.</p>
</>
)}
</>
)
}
export default IndexPage
The outcome of my console.log inside the my IndexPage component is the following when I first load the page or whenever the page is reloaded:
app {userIsLoggedIn: false}
app {userIsLoggedIn: true}
This means my page is re-rendering and my content is flickering between content which is hidden/shown when a user is logged in. Is there a way to avoid this and make the state more instant? I'm open for any suggestions :)

Okay so I found out what helps my specific case. Instead of using the context api to keep an app state (which will be reset to it's default value when reloaded, hence the flickering between states) I use localStorage to save if a user is logged in in combination with my authListener function.
This is the auth util I added:
// Firebase
import firebase from "../config/firebase"
export const isBrowser = () => typeof window !== "undefined"
export const getUser = () => {
if (isBrowser()) {
const user = window.localStorage.getItem("user")
if (user !== null) {
return JSON.parse(user)
} else {
return ""
}
}
}
export const setUser = (email: string | null) =>
isBrowser() && window.localStorage.setItem("user", JSON.stringify(email))
export const isLoggedIn = () => {
const user = getUser()
return !!user
}
export const logout = () => {
return new Promise(resolve => {
firebase
.auth()
.signOut()
.then(() => {
setUser("")
resolve()
})
})
}
and inside my AppWrapper my authListener function now looks like this:
import { setUser, logout } from "../../utils/auth"
const authListener = () => {
firebase.auth().onAuthStateChanged(user => {
if (user && user.emailVerified) {
setUser(user.email)
} else {
logout()
}
})
}
useEffect(() => {
authListener()
})
Anywhere in my app I can use the isLoggedIn() util to check if the user is actually logged in without having the flickering content.
If I manually delete or alter the localStorage user this will instantly be refreshed by the authListener function when anything changes in the app.

Related

Keep state in react inside context after logged in?

I'm coding a web react app with sign in. In my server side I'm using express, jwt and sending a httpOnly cookie with the token when succesfully log in. When user logs in, I'm trying to keep state in the client (e.x, loggedIn = true) inside of a context, but every time that context is rendered it comes back to default state (undefined). How could i keep that state in memory?
My user route, that works as expected (backend):
users.post('/login',async (req, res) => {
try {
const {userName,userPass} = req.body
const u = await models.User.findOne({
userid: userName
})
if (!u) res.status(404).end()
if (bcrypt.compare(userPass,u.password)) {
// JWT TOKEN
const t = c_auth(u._id)
res.status(200).cookie("session",t,{
httpOnly:true
}).end()
} else {
res.status(404).end()
}
} catch (e) {
console.log({'ERROR':e})
res.status(500).end()
}
})
My user provider that returns true when request is ok (client):
get: async (user,pass) => {
try {
const req = await axios.post('/users/login',{
userName: user,
userPass: pass
})
if (req.status === 200) {
return true
} else {
return false
}
} catch (e) {
console.log({'ERROR':e})
return false
}
}
Login submit function (client):
import {useAuth} from '../../../../contexts/AuthContext.js'
const {setLoggedIn} = useAuth()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
// ^---> Trying to set loggedIn state to true in context
window.location.replace('/')
} else {
alert(`Incorrect.`)
}
}
Auth context:
const AuthContext = createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider ({children}) {
const [loggedIn,setLoggedIn] = useState()
console.log(loggedIn)
// ^---> Getting true after login,
// undefined (default useState) after re-render
const value = {
loggedIn,
setLoggedIn
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
App.js:
import {AuthProvider} from './contexts/AuthContext'
function App() {
return (
<AuthProvider>
<div className="App">
<Navigation/>
<main>
<Router>
<Routes>
...Routes
</Routes>
</Router>
</main>
</div>
</AuthProvider>
)
}
I suppose that react memo could be the solution, but I don't understand quite well how it works. Also, is it correct not to use the setLoggedIn in the AuthContext itself? I tried to call the login or sign up function (second piece of code) from the AuthContext, but can't set state since its unmounted. Would need to do that inside a useEffect and that's not what I'm looking for since I wouldn`t be able to export that function. All help is appreciated.
EDIT: fixed
The problem solved after changing window.location.replace to the useNavigate hook from react-router-dom, causing a refresh:
import {useAuth} from '../../../../contexts/AuthContext.js'
import {useNavigate} from 'react-router-dom'
const {setLoggedIn} = useAuth()
const navigate = useNavigate()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
navigate('/')
} else {
alert(`Incorrect.`)
}
}
Also in my navbar I was using <a href> tags instead of <Link to> from 'react-router-dom'. That fixes the problem when I go to a different page from the navbar, so it doesn't 'refresh'.

Retain GlobalState after page reload

Consider the code below:
Ignore all the axios request:
Login.js
const login = async () => {
let newLogin = {
email,
password,
};
await axios
.post("http://localhost:5000/login", newLogin)
.then((response) => {
if (response.data === 404) {
setUserError("User not found. Please Sign Up");
} else if (response.data === 403) {
setPasswordError("Incorrect Password");
} else {
dispatch({type:'LOG_IN',payload:{LoggedIn:true , currentUser:"Curent user"}})
navigate("/");
}
})
.catch((err) => console.log(err));
};
This pages updates the GlobalContext by changing loggedIn to true and currentUser to "fa".
Profile.js
import React , {useContext} from 'react'
import '../App.css'
import {Context} from '../GlobalState/Store'
const Profile = () => {
const [state,dispatch] = useContext(Context);
console.log(state);
return (
<div className="profile-page">
<div className="personal">
<h2>First Name:</h2>
<h2>Last Name:</h2>
<h2>Resistered Email:</h2>
<h2>User Name:</h2>
</div>
<div className="info">
<h2>Father Name:</h2>
<h2>Gender:</h2>
<h2>CNIC:</h2>
<h2>Blood Group:</h2>
<h2>Contact:</h2>
</div>
<div className="uni-info">
<h2>Designation:</h2>
<h2>Department:</h2>
<h2>Batch:</h2>
<h2>Roll No:</h2>
<h2>Enrolement:</h2>
</div>
</div>
);
}
export default Profile
This gets the state and logs it.
Here are the Reducer.js and GlobalContext.js:
const Reducer = (state,action) =>{
switch(action.type){
case 'LOG_IN':
return {
userEmail: action.payload.currentUser,
loggedIn: action.payload.LoggedIn
}
case 'LOG_OUT':
return {
userEmail: action.payload.currentUser,
loggedIn: action.payload.LoggedIn
}
default:
return state;
}
}
export default Reducer;
import React,{useReducer , createContext} from 'react'
import Reducer from './Reducer';
const initialState ={
loggedIn: false,
currentUser:''
}
const Store = ({children}) => {
const [state , dispatch] = useReducer(Reducer,initialState)
return (
<Context.Provider value={[state , dispatch]} >
{children}
</Context.Provider>
)
}
export const Context = createContext(initialState)
export default Store
Everything works beautifully and once I log in, I'm redirected to homepage as I'm supposed to and the correct state is being logged.
But once I refresh on the home page everything is reverted back to the initial state.
I need to retain the state as it will be used to apply the 'IfLoggedIn' logic.
Thanks in advance.
Cookie time (local storage)
Your login request will give your site a cookie that should be stored. when loading the page before anything check if the cookie/session is valid and set the state accoridingly.
Your react state cannot be staved (directly) between hard refreshes

Server Error Error: Invalid hook call. Hooks can only be called inside of the body of a function component in _app.js

I am a newbie in React and Next JS, I want to set initial auth user data on initial load from the __app.js. But using dispatch throwing error "Invalid hook call". I know according to docs calling hooks in render function is wrong. but I am looking for an alternate solution to this.
How I can set auth data one-time so that will be available for all the pages and components.
I am including my code below.
/contexts/app.js
import { useReducer, useContext, createContext } from 'react'
const AppStateContext = createContext()
const AppDispatchContext = createContext()
const reducer = (state, action) => {
switch (action.type) {
case 'SET_AUTH': {
return state = action.payload
}
default: {
throw new Error(`Unknown action: ${action.type}`)
}
}
}
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, {})
return (
<AppDispatchContext.Provider value={dispatch}>
<AppStateContext.Provider value={state}>
{children}
</AppStateContext.Provider>
</AppDispatchContext.Provider>
)
}
export const useAuth = () => useContext(AppStateContext)
export const useDispatchAuth = () => useContext(AppDispatchContext)
/_app.js
import 'bootstrap/dist/css/bootstrap.min.css'
import '../styles/globals.css'
import App from 'next/app'
import Layout from '../components/Layout'
import { mutate } from 'swr'
import { getUser } from '../requests/userApi'
import { AppProvider, useDispatchAuth } from '../contexts/app'
class MyApp extends App {
render() {
const dispatchAuth = useDispatchAuth()
const { Component, pageProps, props } = this.props
// Set initial user data
const setInitialUserData = async () => {
if (props.isServer) {
const initialData = {
loading: false,
loggedIn: (props.user) ? true : false,
user: props.user
}
const auth = await mutate('api-user', initialData, false)
dispatchAuth({
type: 'SET_AUTH',
payload: auth
})
}
}
//----------------------
// Set initial user data
setInitialUserData()
//----------------------
return (
<AppProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AppProvider>
)
}
}
MyApp.getInitialProps = async (appContext) => {
let isServer = (appContext.ctx.req) ? true : false
let user = null
let userTypes = {}
// Get user server side
if (isServer) {
await getUser()
.then(response => {
let data = response.data
if (data.status == true) {
// Set user
user = data.data.user
userTypes = data.data.user_types
//---------
}
})
.catch(error => {
//
})
}
//---------------------
return {
props: {
user,
userTypes,
isServer
}
}
}
export default MyApp
I believe this is the intended use of the useEffect hook with an empty array as its second argument:
https://reactjs.org/docs/hooks-effect.html
import {useEffect} from 'react'
class MyApp extends App {
useEffect(()=> {
setInitialUserData()
},[])
render() {
...
}
}

Next JS Redux not update server store

The Redux Update operations I made on the client-side in Next JS are not updated in the server store.
Hello. I have a problem. I'm developing an SSR application with Next JS. I have provided the next js link with the next-redux-wrapper. State update operations can be provided. State updates I made on the server-side can be viewed on the client-side. The redux updates I made on the client-side also appear on the client-side, but when I refresh the page, it returns to the previous position. Sample scenario:
Users have addresses. Their addresses can be taken from the DB and printed on the screen. DB updates when I add a new address or delete the old address. Along with it, it is updated in the store on the client-side. So far there is no problem. However, when I refresh the page, for example, if there are 4 addresses before updating and I deleted one, after the refresh, it is printed as 4 addresses again. It continues like this until I get data from the server again.
How can I move the client-side store updates to the server-side without having to make requests to the server over and over again?
store.js
// store.js
import { createStore, applyMiddleware } from 'redux';
import { createWrapper } from "next-redux-wrapper";
import thunkMiddleware from 'redux-thunk'
// ROOT REDUCERS
import rootReducer from "../reducers";
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension')
return composeWithDevTools(applyMiddleware(...middleware))
}
return applyMiddleware(...middleware)
}
const store_ = (initialState) => {
return createStore(rootReducer, initialState, bindMiddleware([thunkMiddleware]));
}
const wrapper = createWrapper(store_/*, { debug: true }*/);
export {
wrapper
}
_app.js
// _app.js
const MyApp = ({props, Component, pageProps }) => {
const store = useStore();
if (!store.getState().R_PageSettings.initStore)
{
store.dispatch({
type: HYDRATE,
payload: {
...props.initialState
}
})
}
return (
<>
<Head>
<title>{ variables.meta.title }</title>
</Head>
<Component {...pageProps} />
</>
)
}
const wrappedApp = wrapper.withRedux(MyApp);
export default wrappedApp;
wrappedApp.getInitialProps = async ctx => {
const data = await wrapper.getServerSideProps(
async (req) => {
const { store, ctx } = req;
const reduxStates = store.getState();
let user = reduxStates.R_User.user;
if (!user)
{
const cookies = parseCookies(ctx);
if (cookies.usr && user !== undefined)
{
const getUser = await CustomersController.tokenLoginControl(cookies.usr);
if (getUser && getUser.status)
{
store.dispatch(setUserSSR(getUser.user))
user = getUser.user;
}
else
destroyCookie(ctx, 'usr');
}
}
return {
user
}
}
)(ctx)
return data;
}
action.js
// CONSTANTS
import {
C_User
} from "../constants";
export const setUserSSR = user => {
return {
type: C_User.SET_USER,
payload: {
user
}
}
}
export const setUser = user => dispatch => {
return dispatch({
type: C_User.SET_USER,
payload: {
user
}
})
}
addresspage.js
// addresspage.js
import { connect } from 'react-redux';
import { bindActionCreators } from "redux";
// COMPONENTS
import UserPageLayout from "../UserPagesLayout";
import {
CustomerAddressForm
} from "../../../components";
// CONTROLLERS
import {
CustomersController
} from "../../../controllers";
// ACTIONS
import {
setUser
} from "../../../actions";
const MyAddressPage = connect(({ R_User }) => {
return {
R_User
}
}, dispatch => {
return {
setUser: bindActionCreators(setUser, dispatch)
}
})((props) => {
const addAddressHandle = () => {
props.fullBarOpen(
<CustomerAddressForm confirmHandle={async (address, setLoading) => {
const execute = await CustomersController.addAddress(address);
if (execute.status)
{
await props.setUser(execute.user);
}
else
{
setLoading(false);
}
}}
/>
);
}
return (
<UserPageLayout>
</UserPageLayout>
);
})
export default MyAddressPage;

Can't get currentUser on load

When trying to check if a user is signed in via firebase.auth().currentUser like this:
if (firebase.auth().currentUser === null) {
console.log('User not signed in');
}
Whenever I refresh the page, or navigate around the above returns null (even though I have just logged in).
The weird thing is, that if I log
console.log(firebase.auth().currentUser) // This returns null
console.log(firebase.auth()) // Here I can inspect the object and currentUser exists...!
I don't really know what's going on here. I'm using React and Redux, but it shouldn't really matter I'd say.
Is there a small delay where the firebase initialises and you can't access the currentUser? If so, how can I see it in the log output of firebase.auth()?
This is a commonly asked question.
https://firebase.google.com/docs/auth/web/manage-users
You need to add an observer to onAuthStateChanged to detect the initial state and all subsequent state changes,
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
A simple way is to add a pending state.
Here is a react example using hooks:
// useAuth.ts
import { useState, useEffect } from 'react'
import { auth } from 'firebase'
export function useAuth() {
const [authState, setAuthState] = useState({
isSignedIn: false,
pending: true,
user: null,
})
useEffect(() => {
const unregisterAuthObserver = auth().onAuthStateChanged(user =>
setAuthState({ user, pending: false, isSignedIn: !!user })
)
return () => unregisterAuthObserver()
}, [])
return { auth, ...authState }
}
// SignIn.tsx
import React from 'react'
import { StyledFirebaseAuth } from 'react-firebaseui'
import { useAuth } from '../hooks'
export default function SignIn() {
const { pending, isSignedIn, user, auth } = useAuth()
const uiConfig = {
signInFlow: 'popup',
signInOptions: [
auth.GoogleAuthProvider.PROVIDER_ID,
auth.FacebookAuthProvider.PROVIDER_ID,
],
}
if (pending) {
return <h1>waiting...</h1>
}
if (!isSignedIn) {
return (
<div>
<h1>My App</h1>
<p>Please sign-in:</p>
<StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={auth()} />
</div>
)
}
return (
<div>
<h1>My App</h1>
<p>Welcome {user.displayName}! You are now signed-in!</p>
<a onClick={() => auth().signOut()}>Sign-out</a>
</div>
)
}
The best way to always have access to currentUser is to use vuex and vuex-persistedstate
//Configure firebase
firebase.initializeApp(firebaseConfig);
//When ever the user authentication state changes write the user to vuex.
firebase.auth().onAuthStateChanged((user) =>{
if(user){
store.dispatch('setUser', user);
}else{
store.dispatch('setUser', null);
}
});
The only issue above is that if the user presses refresh on the browser the vuex state will be thrown away and you have to wait for onAuthStateChange to fire again, hence why you get null when you try to access currentUser.
The secret to the above code working all the time is to use vuex-persisted state.
In your store.js file
import Vue from 'vue'
import Vuex from 'vuex'
import firebase from 'firebase/app'
Vue.use(Vuex)
import createPersistedState from "vuex-persistedstate";
export default new Vuex.Store({
plugins: [createPersistedState()],
state: {
user: null
},
getters:{
getUser: state => {
return state.user;
}
},
mutations: {
setUser(state, user){
state.user = user;
}
},
actions: {
setUser(context, user){
context.commit('setUser', user);
},
signIn(){
let provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function (result) {
})
},
signOut(){
firebase.auth().signOut();
}
}
})
You can now protect routes in your router as in the code example below.
import Vue from 'vue'
import Router from 'vue-router'
import Home from '#/components/Home'
import Search from '#/components/Search/Search'
import CreateFishingSite from '#/components/FishingSites/CreateFishingSite'
Vue.use(Router);
import store from './store'
import firebase from 'firebase'
let router = new Router({
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/search/:type',
name: 'Search',
component: Search
},
{
path: '/fishingsite/create',
name: 'CreateFishingSite',
component: CreateFishingSite,
meta: {
requiresAuth: true
}
}
]
})
router.beforeEach(async (to, from, next)=>{
let currentUser = store.state.user;
console.log(currentUser);
let requriesAuth = to.matched.some(record => record.meta.requiresAuth);
if(requriesAuth && !currentUser){
await store.dispatch('signIn');
next('/')
}else{
next()
}
})
If you are looking for a copy and paste Auth route for react with firebase:
const AuthRoute = ({ component: Component, ...rest }) => {
const [authenticated, setAuthenticated] = useState(false)
const [loadingAuth, setLoadingAuth] = useState(true)
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
setAuthenticated(true)
} else {
setAuthenticated(false)
}
setLoadingAuth(false)
})
}, [])
return loadingAuth ? 'loading...' : (
<Route
{...rest}
render={props =>
authenticated ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/user/login' }} />
)}
/>
)
}
Promise-wise, there are three options:
UPDATE: 11/26/22
For Firebase 9+, you could do:
Note: (this.auth) is the Auth object and depends on your framework.
const user1 = await firstValueFrom(authState(this.afa));
const user2 = await firstValueFrom(
new Observable(observer => onAuthStateChanged(this.afa, observer))
);
const user3 = this.afa.currentUser;
// best option
const user1 = await new Promise((resolve: any, reject: any) =>
firebase.auth().onAuthStateChanged((user: any) =>
resolve(user), (e: any) => reject(e)));
console.log(user1);
// sometimes does not display correctly when logging out
const user2 = await firebase.auth().authState.pipe(first()).toPromise();
console.log(user2);
// technically has a 3rd state of 'unknown' before login state is checked
const user3 = await firebase.auth().currentUser;
console.log(user3);
// On component load.
componentDidMount = () => this.getAuthStatus();
// Get firebase auth status.
getAuthStatus = () => {
firebase.auth().onAuthStateChanged((resp) => {
// Pass response to a call back func to update state
this.updateUserState(resp);
});
}
// update state
updateUserState = (resp) => {
this.setState({
user: resp
})
}
// Now you can validate anywhere within the component status of a user
if (this.state.user) { /*logged in*/}
Best approach for this is to use a promise and only instantiate the router after the response, something along the lines of:
store.dispatch('userModule/checkAuth').then(() => {
// whatever code you use to first initialise your router, add it in here, for example
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
})
inside the checkAuth action is where you would have your promise, like so:
checkAuth ({ commit }) {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged(async (_user) => {
if (_user) {
commit('setUser', _user)
} else {
commit('setUser', null)
}
console.log('current user in checkAuth action:', _user)
resolve(true)
})
})
h/t to aaron k saunders - the source of this solution for me.
If you'd like the user to access to a certain page only if he is authenticated and to redirect to the home page if he is not, the following codes might help:
in React:
make a component with the following code:
import { onAuthStateChanged } from "#firebase/auth";
import { Route, Redirect } from "react-router-dom";
import { auth } from "../firebase/config";
import { useState, useEffect } from "react";
const GuardedRoute = ({ component, path }) => {
const [authenticated, setAuthenticated] = useState(false);
const [authCompleted, setAuthCompleted] = useState(false);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setAuthenticated(true);
} else {
setAuthenticated(false);
}
setAuthCompleted(true);
});
}, []);
return authCompleted ? (
authenticated ? (
<Route path={path} component={component} />
) : (
<Redirect to="/" />
)
) : (
""
);
};
export default GuardedRoute;
and in app.js use:
import RouterPage from "./pages/RouterPage";
<GuardedRoute path="/router-page" component={RouterPage} />
in Vue:
at the router file use:
const guardSuccess = (to, from, next) => {
let gUser = auth.currentUser
if (gUser) {
next()
} else {
next({ name: "Home" })
}
}
and in the routes of the page you want to restrict access to add:
{
path: "/router-page",
name: "routerPage",
component: () => import("../views/routerPage.vue"),
beforeEnter: guardSuccess
}
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
var user = firebase.auth().currentUser;
if(user != null){
var io=user.uid;
window.alert("success "+io);
}
} else {
// No user is signed in.
Window.reload();
}
});
first check if user exist then get it id by
firebase.auth().currentUser.uid

Categories