CreateContext not updating children when performin Authenticaion - reactjs

I have a login component from which i try to save the authenticated user's data into the AuthContext using a custom useAuth() hooks. But when i set the value from within login component AuthContext never get updated and the auth object is null, but when i set the AuthContext from within another componenet it got updated as expected and i could access it from other component. what might be the reason behind this weird behaviour
import useAuth from "../../hooks/useAuth";
const {auth, setAuth} = useAuth();
useEffect(() => {
console.log(auth)
}, [auth]);
const submitHandler = async (e) => {
e.preventDefault();
try {
const response = await axios.post(LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
);
const accessToken = response.data ? response.data.accessToken : null;
const user = response.data ? response.data.user : null;
// setAuth({
// accessToken:accessToken,
// user:user
// });
const obj = {
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIser5taSI6Im1AMkh0a3k2KnQ5aWZlIn0.eyJpc3MiOiJodHRwczpcL1wvYXBpLmNoZWNrc3RhbGwuY29tIiwiYXVkIjoiaHR0cHM6cdfrt2FwaS5jaGVja3N0YWxsLmNvbSIsImp0aSI6Im1AMkh0a3k2KnQ5aWZlIiwiaWF0IjoxNjY3NTkwMjg3LCJleHAiOjE2Njc1OTA1ODcsInVpZCI6MX0.4LYi3eDW6mfKB9H_vOjhfqttKoT1dGUdJuisU3esdwt",
"user": {
"username": "bob01",
"email": "bob#yahoo.com",
"created_at": 1659239223,
"full_name": "Bob L",
"role": 40,
"phone_number": "199765432",
"last_login": 16647884254
}
}
setAuth(obj);
setUsername('');
setPassword('');
//navigate(-1);
setModalShow(false);
}catch(err){
if (err.response) {
//do something
} else{
//do something
}
}
}
after async call the useEffect got called and log the result as expected but when i set the same result into the AuthContext through the useAuth hooks it never go updated
Below is my useAuth hooks
import { useContext, useDebugValue } from "react";
import AuthContext from "../context/AuthProvider";
const useAuth = () => {
const { auth } = useContext(AuthContext);
if (auth === undefined) {
throw new Error("useUserContext can only be used inside AuthProvider");
}
return useContext(AuthContext);
}
export default useAuth;
and also my AuthContext Provider
import React ,{ createContext, useState, useMemo } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
const [persist, setPersist] = useState(JSON.parse(localStorage.getItem("persist")) || false);
const value = useMemo(() => ({
auth, setAuth ,persist ,setPersist
}), [auth, setAuth, persist,setPersist]);
return (
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>
)
}
export default AuthContext;

Related

React redux slice - Accessing to state changes after logged in

I'm trying to use a combination of a slice react redux to store my token credentials with a dispatch action to update the state in a call to my API.
Finally in the login component i was trying to verify if somehow that token state changed to verify if user can access.
But this condition in my component loginHandler function:
if(auth){ history.push('/wallet') }
Seems to be always empty.
This is my redux slice:
import {createSlice} from "#reduxjs/toolkit";
const authSlice = createSlice({
name: 'auth',
initialState: {
token: {
access: '',
refresh: ''
}
},
reducers: {
login(state, action){
state.token = action.payload
},
logout(state){
state.token.access = ''
state.token.refresh = ''
}
}
})
export const authActions = authSlice.actions
export default authSlice
This is my action:
import http from "../http-common";
import {authActions} from "./auth-slice";
export const login = (data) => {
return async (dispatch) => {
const loginRequest = async () => {
const response = await http.post('/api/token/login', data);
return response.data;
};
try {
const sessionsData = await loginRequest();
console.log(sessionsData)
localStorage.setItem("token", JSON.stringify(sessionsData))
console.log(sessionsData)
dispatch(
authActions.login(sessionsData)
)
} catch (error) {
console.log(error)
}
};
}
And inside my component i'm using the dispatch to my action and the useSelector to track changes to my store state:
import React, {useState, useContext} from 'react';
import {useHistory} from "react-router-dom";
import LoginForm from '../../components/forms/LoginFormPage'
import {useSelector, useDispatch} from "react-redux";
import {login} from "../../store/auth-actions";
const Login = () => {
let history = useHistory();
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false)
const auth = useSelector((state) => state.auth.token.access);
const handleLogin = (data) => {
console.log(auth)
setIsLoading(true)
dispatch(login({email: data.email, password: data.password})).then()
console.log(auth)
if(auth){
history.push('/wallet')
}
setIsLoading(false)
}
return (
<LoginForm
onLoginSubmit={handleLogin}
isLoading={isLoading}/>
)
}
export default Login;

NextJS - Protected route not working creating the following error (Unexpected token o in JSON at position 1 at JSON.parse (<anonymous>))

Trying to implement protected routes but there is an error that occurs at the AuthUserProvider component. What I'm hoping to do is pass the firebase user information via the routes so I can send the user to the login page if they're not signed in. What is happening is that the user file is returning null even when a sign-in is performed.
Here is the error text:
Uncaught SyntaxError: Unexpected token o in JSON at position 1
at JSON.parse ()
at getUserFromCookie (userCookies.js:9)
at useUser.js:47
I've called the user data here and then sent it through to the children. Or I think I have. Here is the code in the AuthUserProvider file that i've attempted:
import { createContext, useContext, Context } from "react";
import useUser from "../firebase/useUser";
const authUserContext = createContext({
user: null,
});
export function AuthUserProvider({ children }) {
const auth = useUser();
return (
<authUserContext.Provider value={auth}>{children}</authUserContext.Provider>
);
}
// custom hook to use the authUserContext and access authUser and loading
export const useAuth = () => useContext(authUserContext);
Here is the useUser code where I have set up to call the user information via a token:
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import firebase from "firebase/app";
import "firebase/auth";
import initFirebase from "./initFirebase";
import {
removeUserCookie,
setUserCookie,
getUserFromCookie,
} from "./userCookies";
import { mapUserData } from "./mapUserData";
initFirebase();
export default function useUser() {
const [user, setUser] = useState();
const router = useRouter();
const logout = async () => {
return firebase
.auth()
.signOut()
.then(() => {
// Sign-out successful.
router.push("/auth");
})
.catch((e) => {
console.error(e);
});
};
useEffect(() => {
// Firebase updates the id token every hour, this
// makes sure the react state and the cookie are
// both kept up to date
const cancelAuthListener = firebase.auth().onIdTokenChanged((user) => {
if (user) {
const userData = mapUserData(user);
setUserCookie(userData);
setUser(userData);
} else {
removeUserCookie();
setUser();
}
});
const userFromCookie = getUserFromCookie();
if (!userFromCookie) {
router.push("/");
return;
}
setUser(userFromCookie);
return () => {
cancelAuthListener();
};
}, []);
return { user, logout };
}
Here is the getUseFromCookie & setUserCookie code:
import cookies from "js-cookie";
export const getUserFromCookie = () => {
const cookie = cookies.get("auth");
if (!cookie) {
return;
}
return JSON.parse(cookie);
};
export const setUserCookie = (user) => {
cookies.set("auth", user, {
expires: 1 / 24,
});
};
export const removeUserCookie = () => cookies.remove("auth");

React Custom Hooks Circular Dependency

I have two custom hooks i.e useFetch and useAuth. useAuth has all API calls methods (e.g logIn, logOut, register, getProfile etc) and they use useFetch hook method for doing API calls. useFetch also uses these methods for example logOut method when API return 401, setToken etc. So, they both need to share common methods. But that results into circular dependency and call size stack exceeded error. How to manage this
UseFetch.js
import React, { useState, useContext } from "react";
import { AuthContext } from "../context/authContext";
import { baseURL } from "../utils/constants";
import { useAuth } from "./useAuth";
const RCTNetworking = require("react-native/Libraries/Network/RCTNetworking");
export const useFetch = () => {
const {token, setAuthToken, isLoading, setIsLoading, logIn, logOut} = useAuth();
const fetchAPI = (method, url, body, isPublic, noBaseURL) => {
setIsLoading(true);
const options = {
method: method
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
};
return fetch(url, options, isRetrying).then(() => {
......
})
......
};
return { fetchAPI };
};
UseAuth.js
import React, { useContext, useEffect } from "react";
import { AuthContext } from "../context/authContext";
import { useFetch } from "./useFetch";
export const useAuth = () => {
const {
removeAuthToken,
removeUser,
setUser,
...others
} = useContext(AuthContext);
const { fetchAPI } = useFetch();
const register = (body) => {
return fetchAPI("POST", "/customers/register", body, true);
};
const logIn = (body) => {
return fetchAPI("POST", "/customers/login", body, true);
};
const logOut = () => {
return (
fetchAPI("POST", "/customers/logout")
.catch((err) => console.log("err", err.message))
.finally(() => {
removeAuthToken();
removeUser();
})
);
......
};
return {
...others,
register,
logIn,
logOut,
};
};

How to set the default react context value as data from firestore?

I'm building a workout program planner app, the workout program is handled in the app with a SetProgram context and is updated with a custom hook called useProgram. I need that when the user logins that the app will fetch data from firestore and display the user's workout program, how can I do this? Keeping in mind that the useProgram hook is also used throughout the app to edit and update one's workout program.
App.tsx
import React, { useContext, useEffect, useState } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from "./Router";
import FirebaseApp from "./firebase";
import SetProgram from "./context/program";
import { useProgram } from "./components/hooks/useProgram";
import firebaseApp from "./firebase/firebase";
import { useAuthState } from "react-firebase-hooks/auth";
function App() {
const program = useProgram();
const day = useDay();
const [user, loading, error] = useAuthState(firebaseApp.auth);
return (
<div className="App">
<SetProgram.Provider value={program}>
<Router>
<AppRouter />
</Router>
</SetProgram.Provider>
</div>
);
}
export default App;
firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from "./config";
class Firebase {
auth: firebase.auth.Auth;
user: firebase.User | null | undefined;
db: firebase.firestore.Firestore;
userProgram: {} | undefined;
constructor() {
firebase.initializeApp(firebaseConfig);
this.auth = firebase.auth();
this.db = firebase.firestore();
}
async register() {
if (this.user) {
this.db.collection("users").doc(this.user.uid).set({
name: this.user.displayName,
email: this.user.email,
userId: this.user.uid,
program: {},
});
}
}
async getResults() {
return await this.auth.getRedirectResult().then((results) => {
console.log("results.user", results.user);
if (!results.additionalUserInfo?.isNewUser) {
this.getProgram();
} else {
this.register();
}
});
}
async login(
user: firebase.User | null | undefined,
loading: boolean,
error: firebase.auth.Error | undefined
) {
const provider = new firebase.auth.GoogleAuthProvider();
return await this.auth
.signInWithRedirect(provider)
.then(() => this.getResults());
}
async logout() {
return await this.auth.signOut().then(() => console.log("logged out"));
}
async updateProgram(user: firebase.User, program: {}) {
if (this.userProgram !== program) {
firebaseApp.db
.collection("users")
.doc(user.uid)
.update({
program: program,
})
.then(() => console.log("Program updated successfully!"))
.catch((error: any) => console.error("Error updating program:", error));
} else {
console.log("No changes to the program!");
}
}
async getProgram() {
firebaseApp.db
.collection("users")
.doc(this.user?.uid)
.get()
.then((doc) => {
console.log("hello");
if (doc.exists) {
this.userProgram = doc.data()?.program;
console.log("this.userProgram", this.userProgram);
} else {
console.log("doc.data()", doc.data());
}
});
}
}
const firebaseApp = new Firebase();
export default firebaseApp;
programContext.tsx
import React from "react";
import Program, { muscleGroup, DefaultProgram } from "../interfaces/program";
export interface ProgramContextInt {
program: Program | undefined;
days: Array<[string, muscleGroup]> | undefined;
setProgram: (p: Program) => void;
}
export const DefaultProgramContext: ProgramContextInt = {
program: undefined,
days: undefined,
setProgram: (p: Program): void => {},
};
const ProgramContext = React.createContext<ProgramContextInt>(
DefaultProgramContext
);
export default ProgramContext;
useProgram.tsx
import React from "react";
import {
ProgramContextInt,
DefaultProgramContext,
} from "../../context/program";
import Program, { muscleGroup } from "../../interfaces/program";
import { useAuthState } from "react-firebase-hooks/auth";
import firebaseApp from "../../firebase";
export const useProgram = (): ProgramContextInt => {
const [user] = useAuthState(firebaseApp.auth);
const [program, setEditedProgram] = React.useState<Program | undefined>();
const [days, setProgramDays] = React.useState<
[string, muscleGroup][] | undefined
>(program && Object.entries(program));
const setProgram = React.useCallback(
(program: Program): void => {
firebaseApp.updateProgram(user, program);
setEditedProgram(program);
setProgramDays(Object.entries(program));
},
[user]
);
return {
program,
days,
setProgram,
};
};
There are two ways to handle this in my opinion:
Update the ProgramContext to make sure that the user is logged in
Wrap the App or any other entry point from whence you need to make sure that the user is logged in, in a separate UserContextProvider
Let's talk about the latter method, where we can wrap in a separate context called UserContext. Firebase provides us a listener called onAuthStateChanged, which we can make use of in our context, like so:
import { createContext, useEffect, useState } from "react";
import fb from "services/firebase"; // you need to define this yourself. It's just getting the firebase instance. that's all
import fbHelper from "services/firebase/helpers"; // update path based on your project organization
type FirestoreDocSnapshot = firebase.default.firestore.DocumentSnapshot<firebase.default.firestore.DocumentData>;
const UserContext = createContext({ user: null, loading: true });
const UserContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const userContext = { user, loading };
const updateUser = (snapShot: FirestoreDocSnapshot) => {
setUser({
id: snapShot.id,
...snapShot.data,
});
};
const authStateListener = async (authUser: firebase.default.User) => {
try {
if (!authUser) {
setUser(authUser);
return;
}
const fbUserRef = await fbHelper.findOrCreateFirestoreUser(authUser)
if ("error" in fbUserRef) throw new Error(fbUserRef?.error);
(fbUserRef as FirestoreUserRef).onSnapshot(updateUser)
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
useEffect(() => {
const unSubscribeAuthStateListener = fb.auth.onAuthStateChanged(authStateListener);
return () => unSubscribeAuthStateListener();
}, [])
return (
<UserContext.Provider value={userContext}>
{children}
</UserContext.Provider>
)
};
export default UserContextProvider;
Where the helper can be something like this:
export type FirestoreUserRef = firebase.default.firestore.DocumentReference<firebase.default.firestore.DocumentData>
const findOrCreateFirestoreUser = async (authUser: firebase.default.User, additionalData = {}): Promise<FirestoreUserRef | { error?: string }> => {
try {
if (!authUser) return { error: 'authUser is missing!' };
const user = fb.firestore.doc(`users/${authUser.uid}`); // update this logic according to your schema
const snapShot = await user.get();
if (snapShot.exists) return user;
const { email } = authUser;
await user.set({
email,
...additionalData
});
return user;
} catch (error) {
throw error;
}
};
Then wrap your other context which provides firestore data, within this UserContextProvider. Thus whenever you login or logout, this particular listener be invoked.

React Context API - create context from axios response

How to create context from axios response?
import React, { createContext } from 'react';
import axios from 'axios';
const AppContext = createContext({
lang: 'en',
currency: 'USD',
name: 'Demo store'
});
const token = JSON.parse(localStorage.getItem('token'));
axios.get('http://localhost:3000/storedata', {
headers: {
'Authorization' : `Bearer ${token}`
}
})
.then(response => {
//set context
})
.catch(error => {
});
export default AppContext;
Header.js
import React, { useContext } from 'react';
import AppContext from '../../context/AppContext';
const Header = props => {
const appContext = useContext(AppContext);
console.log(appContext);
}
This is my code. I was storing lang, currency and name in local storage when login and getting values from local storage wherever need.
Now I want to store in global state when login and pass value to other components. I am not able to create context from API call.
In my react apps I fetch data to context like that
// context.js
export const MyContextData = createContext(null); // null is the default value
export const MyContext = (props) => {
const [myState, SetMyState] = useState(null);
useEffect(() => {
axios
.get("http://localhost:3000/storedata", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
setMyState(response); // update your state
})
.catch((error) => {
// handle errors
});
}, []);
return (
<MyContextData.Provider
value={myState} // value of your context
>
{props.children}
</MyContextData.Provider>
);
};
// index.js
//wrap your app with the context so the whole app re-render when the context update
<MyContext>
<App />
</MyContext>;
// app.js
const App = () => {
const context = useContext(MyContextData);
// if the context is null then the data have not been fetched yet
if (!context) {
return; // maybe a loading indicator
} else {
return; //data have been fetched and you can use it
}
};
You need to create a context provider and store the settings in a state:
Context
const AppContext = createContext();
const ContextProvider = () => {
const [settings, setSettings] = useState({
lang: 'en',
currency: 'USD',
name: 'Demo store'
});
// In your axios call, use setSettings to update the settings
return <Context.Provider value={{settings}}/>
};

Resources