What can I do to use a hook inside a function? - reactjs

I want to make a Logout function when the token has expired. There is an AuthProvider in my application:
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ token: localStorage.getItem("access_token") });
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
Now that the token has expired I need to call the setAuth hook and write an empty token there:
const logout = () =>{
const axiosInstance = axios.create({
withCredentials: true
})
axiosInstance.get("http://localhost:8080/api/auth/logout")
.then((response) => {
console.log(response)
})
.catch((error) => {
console.log(error.config);
});
window.location.href = '/auth'
};
const Logout = () => {
const {auth,setAuth} = useAuth();
const token = '';
setAuth({token});
localStorage.removeItem("access_token");
localStorage.clear();
logout()
};
export default Logout;
I am exporting this function in another file and want to call if the backend returns a response about an expired token.
const getStockData = async () => {
return instance.get(`/api/stock/symbols/${slug}`);
}
useEffect(() => {
(async () => {
const response = await getStockData();
console.log(response)
const data = response.data;
const stockInfo = data.chart.result[0];
console.log(stockInfo);
setPrice(stockInfo.meta.regularMarketPrice.toFixed(2));
setPriceTime(new Date(stockInfo.meta.regularMarketTime * 1000));
setSymbol(stockInfo.meta.symbol);
const quote = stockInfo.indicators.quote[0];
const prices = stockInfo.timestamp.map((timestamp, index) => ({
x: new Date(timestamp * 1000),
y: [quote.open[index], quote.high[index], quote.low[index], quote.close[index]].map(round)
}));
setPriceInfo([{
data: prices,
}]);
setStockData({ data });
})().catch(
(error) =>{
Logout()
}
);
}, []);
Here getStockData can return 403 if the token has expired.
But of course I get an error saying that the hook can't be used in a function like that. And I can't find a solution how to wrap or to do something similar so that this function can be called?

React doesn't let you initialize hooks inside of non-component functions. Instead, you can initialize the hook on the component level and let whatever function needs the hook's values to accept them as arguments.
const Logout = (auth, setAuth) => {
const token = '';
setAuth({token});
localStorage.removeItem("access_token");
localStorage.clear();
logout()
};
export default Logout;
// Initialize the hook at the component level
const {auth, setAuth} = useAuth();
.catch(
(error) =>{
// then pass the values from above to this function
Logout(auth, setAuth)
}
);

Related

Persistent Authentication React using UseEffect and Intervals - function not firing

I am attempting to leverage my refresh token backend point within my JWT setup for my React app.
The AuthContext file below works well beside the useEffect and interval. It would seem that my attempt to check the validity of the token every 10 secs is not working. Neither console logs in the refresh function are logged. All ideas very welcome.
import React, { useEffect, useState, useRef} from 'react';
import RefreshToken from '../Services/RefreshToken';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
rToken: '',
tokenExpiry:'',
login: (token) => {},
logout: () => {},
});
export const AuthContextProvider = (props) => {
const [token, setToken] = useState(null);
const [refreshToken, setrefreshToken] = useState(null)
const [tokenExpiry, setTokenExpiry] = useState(null)
let interval = useRef(null);
const userIsLoggedIn = !!token;
const loginHandler = (token,rToken) => {
setToken(token);
setrefreshToken(rToken);
var current = new Date();
current.setHours(current.getHours(),current.getMinutes()+4,0,0);
setTokenExpiry(current)
};
const logoutHandler = () => {
setToken(null);
setrefreshToken(null);
};
const contextValue = {
token: token,
isLoggedIn: userIsLoggedIn,
rToken: refreshToken,
tokenExpiry: tokenExpiry,
login: loginHandler,
logout: logoutHandler,
};
const refresh =(timenow)=>{
console.log("checking")
if(tokenExpiry <= timenow){
console.log("refresh")
try {
const response = RefreshToken(token, refreshToken);
loginHandler(response.token, response.refreshToken);
} catch (err) {
console.log(err);
logoutHandler();
}
}
}
useEffect(() => {
const timeNow = new Date();
interval.current = setInterval(refresh(timeNow), 10000);
return () => clearInterval(interval.current);
}, [])
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;

UseContext returns null in getServer side props NextJS

I have used the context in other places, such as login, database functions, and more. However, when I try to run functions or variables inside my context in places such as custom api's or getServerSideProps, it returns the following error, TypeError: Cannot read properties of null (reading 'useContext'). I am attaching my auth context, my initialization of the context, and the getServerSideProps function that is returning an error
_app.js
import RootLayout from '../components/Layout'
import { AuthProvider } from '../configs/auth-context'
import '../styles/globals.css'
export default function App({ Component, pageProps }) {
return (
<AuthProvider >
<RootLayout>
<Component {...pageProps} />
</RootLayout>
</AuthProvider>
)}
auth-context
import React, { useContext, useState, useEffect, useRef } from 'react'
import { auth, db, provider } from './firebase-config'
import { GoogleAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, onAuthStateChanged, signInWithPopup } from 'firebase/auth'
import { doc, getDoc, setDoc } from 'firebase/firestore'
import {useRouter} from 'next/router';
const AuthContext = React.createContext({currentUser: {uid: "TestUid", email:"Testeremail#email.com"}})
export function UseAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const router = useRouter();
const [currentUser, setCurrentUser] = useState({uid: "TestUid", email:"Testeremail#email.com"})
const [loading, setLoading] = useState(true)
async function signup(email, password) {
createUserWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error);
})
return
}
async function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
.then(async (result) => {
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user;
}).catch((error) => {
console.error(error)
})
}
function logout() {
router.push('/')
return signOut(auth)
}
async function googleSignIn() {
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then(async (result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
await userToDb(user);
router.push('/portfolio');
return user
}).catch((error) => {
console.log(error)
// const errorCode = error.code;
// const errorMessage = error.message;
// The email of the user's account used.
// const email = error.customData.email;
// The AuthCredential type that was used.
// const credential = GoogleAuthProvider.credentialFromError(error);
} )
}
const userToDb = async (user) => {
// await setDoc(doc(db, "users", user.uid), {
// userEmail: user.email,
// userID: user.uid
// }, {merge: false})
let currentRef = doc(db, 'users', user.uid)
let currentUserID = user.uid;
let currentEmail = user.email;
await setDoc(currentRef, {
userEmail: currentEmail,
userID: currentUserID
}, {merge: false})
}
function fixData(docs) {
console.log("this works")
// setDocuments(docs);
let retMap = new Map();
if (currentUser !== null) {
docs?.map(function(doc) {
console.log(doc)
let tic = doc.stockTicker
let data = {
shares: doc.shares,
price: doc.price,
type: doc.type
}
if(!retMap.has(tic)) {
retMap.set(tic, [data]);
console.log(tic + " " + data)
// setMap(new Map(datamap.set(tic, {shares: shares, averagePrice: price})))
}
else {
let x = retMap.get(tic);
x.push(data);
}
})
console.log(retMap)
return retMap;
}
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
signup,
logout,
googleSignIn,
fixData
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
getServerSideProps
export async function getServerSideProps() {
let allDocs = []
let avgDocs = []
const {currentUser} = UseAuth()
return {
props: {allDocs, avgDocs}
}
}
I don't know the correct answer, but hooks should be used in components and hooks without exception to ssr.

useState set method inside useEffect hook not updating till refreching the page

I am trying to update a user when a token was changed. I have two hooks which are useToken (deals with token when login and logout events ) and useUser (update the user after any change in token value). I used them inside a HeaderHelper which is a functional component returns a Header Component that accept user, token, setToken and navigate as props. In the latter, I write a function called afterLogin to invoke setToken in the aim of updating the token field and invoke also a navigate hook to redirect to the appropriate route.
The problem is after logging in, the user field did not get any value and stay "null", Although the setToken and useEffect were invoked. To get the result that I aim to get I should to refresh the browser and it's obviously not practical.
quote NOTE: useUser has a token field as a part of its state. and the HeaderHelper has a user and token as a part of its state.
here is the code of useToken Hook:
export const useToken = () => {
const [ token, setToken ] = useState(()=>{
return sessionStorage.getItem('token');
});
function setTokenStorage (newToken) {
sessionStorage.setItem('token', newToken);
setToken(newToken);
}
return [token, setTokenStorage ];
}
here is the code of useUser Hook:
export const useUser = () => {
const [ token ] = useToken();
const getPayloadFromToken = token => {
console.log(token);
const encodedPayload = token.split('.')[1];
console.log(JSON.parse(atob(encodedPayload)), ': encodedPayload');
return JSON.parse(atob(encodedPayload));
}
const [ user, setUser] = useState(()=>{
// console.log('token from user useState', token , '***');
if(!token) {
console.log(token);
return {};
};
// console.log(getPayloadFromToken(token));
return getPayloadFromToken(token);
});
useEffect( ()=>{
console.log('token from user useEffect', token , '***');
if(!token) {
console.log('user will become null');
setUser({});
}else{
console.log(' useEffect ');
setUser(getPayloadFromToken(token));
}
},[token]);
return [user];
}
Here is the code of HeaderHelper Component:
export const HeaderHelper = (props) => {
const [user] = useUser();
const navigate = useNavigate();
const [token, setToken] = useToken();
return <Header navigate={ navigate } token={token} setToken={ setToken } user={ user } {...props}/>
}
Here is the code of Header Component:
class Header extends Component{
constructor (props){
super(props);
this.state = { ... }
}
handleLogin(e){
e.preventDefault();
this.setState({successSingIn : false});
fetch('http://localhost:8000/api/login', {
method: 'POST',
body: JSON.stringify({
"email" : this.email.value,
"password" : this.password.value,
"type" : this.type.value,
}),
headers: { "Content-Type" : "application/json"},
credentials: "same-origin"
}).then(response => {
if (response.ok){
return response;
}else{
const error = new Error('Error ' + response.status + ' : ' + response.statusText);
error.response = response;
throw error;
}
}).then(response => response.json())
.then(response => {
console.log('response from backend: ' + JSON.stringify(response));
if (response.status) {
this.setState({ failToLogIn : '' });
this.afterLogin(response.user.token);
}else{
this.setState({ failToLogIn : 'something wrong, try again with valid information!' });
}
})
.catch(error => {
this.setState({ failToLogIn : 'something wrong' });
console.log('something wrong with login process\n' , error.message)
});
}
}
and here is the code of afterLogin function:
afterLogin = (userToken) => {
this.toggleLoginModel();
console.log('userToken: ', userToken);
this.props.setToken(userToken);
this.props.navigate('/dashboard');
}
Summary: -This is how I think-
-1 the guest make a login request.
-2 afterLogin function invoked after a successfully login.
-3 then, setToken invoked, leads to invoke that function inside useEffect. And cause a re-render for HeaderHelper.
-4 finally, setUser will be invoked and cause a re-render for HeaderHelper.
-5 after all of that a token and user should have values, unfortunately that didn't happen.
So how can I solve this issue?
thank you!
Yeah, looks like each useToken hook maintains its own state. The setToken Header is using is from the useToken whereas nothing updates the token state of the useToken hook that useUsers is using, so the useEffect hook won't retrigger.
What you might try doing is to return the token and setToken from the useToken hook that useUser is using so there's only the one instance.
export const useUser = () => {
const [token, setToken] = useToken();
const getPayloadFromToken = token => {
console.log(token);
const encodedPayload = token.split('.')[1];
console.log(JSON.parse(atob(encodedPayload)), ': encodedPayload');
return JSON.parse(atob(encodedPayload));
}
const [user, setUser] = useState(() => {
// console.log('token from user useState', token , '***');
if (!token) {
console.log(token);
return {};
};
// console.log(getPayloadFromToken(token));
return getPayloadFromToken(token);
});
useEffect( ()=>{
console.log('token from user useEffect', token , '***');
if (!token) {
console.log('user will become null');
setUser({});
} else {
console.log(' useEffect ');
setUser(getPayloadFromToken(token));
}
}, [token]);
return [user, token, setToken];
}
...
export const HeaderHelper = (props) => {
const [user, token, setToken] = useUser();
const navigate = useNavigate();
return (
<Header
{...props}
navigate={navigate}
token={token}
setToken={setToken}
user={user}
/>
);
}

Handling auth with React Hooks

I've been chasing my tail for hours now trying to figure out how to handle auth on my component using firebase and react hooks.
I've created a custom useAuth hook that is intended to handle all the auth behaviors. My thought was to put a useEffect on the root of my component tree that would trigger if the firebase.auth.onAuthStateChanged() ever changed (ie, user is now logged out / logged in.) But, at this point after making a million unsuccessful changes I really don't know what I'm doing anymore.
Here is the code that I have...
RootPage component
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [authStatus]);
...
}
my thought was ok, lets trigger authStatus on mount, but that ends up with me lying about my dependencies. So, in an effort to not lie about my deps, I added authStatus to the deps. Logging out and then logging in results in this:
useAuth hook
const useAuth = () => {
const { fetchUser, resetUser, userData } = useUser();
const { currentUser } = firebaseAuth;
const registerUser = async (username, email, password) => {
try {
const credentials = await firebaseAuth.createUserWithEmailAndPassword(
email,
password
);
const { uid } = credentials.user;
await firebaseFirestore
.collection('users')
.doc(credentials.user.uid)
.set({
username,
points: 0,
words: 0,
followers: 0,
following: 0,
created: firebase.firestore.FieldValue.serverTimestamp(),
});
fetchUser(uid);
console.log('user registered', credentials);
} catch (error) {
console.error(error);
}
};
const loginUser = async (email, password) => {
try {
// login to firebase
await firebaseAuth.signInWithEmailAndPassword(email, password);
// take the current users id
const { uid } = firebaseAuth.currentUser;
// update the user in redux
fetchUser(uid);
} catch (error) {
console.error(error);
}
};
const logoutUser = async () => {
try {
// logout from firebase
await firebaseAuth.signOut();
// reset user state in redux
resetUser();
return;
} catch (error) {
console.error(error);
}
};
const authStatus = () => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
return;
}
console.log('User logged out.');
});
};
return { currentUser, registerUser, loginUser, logoutUser, authStatus };
};
export default useAuth;
I'm relatively certain that react hooks are only meant for reusable pieces of logic, so if the purpose of your hook is to contact firebase in every single component you're using it, along with rerendering and refreshing state every time that component is updated, then it's fine, but you can't use hooks for storing global auth state, which is how auth should be stored.
You're looking for react context instead.
import React, {createContext, useContext, useState, useEffect, ReactNode} from 'react'
const getJwt = () => localStorage.getItem('jwt') || ''
const setJwt = (jwt: string) => localStorage.setItem('jwt', jwt)
const getUser = () => JSON.parse(localStorage.getItem('user') || 'null')
const setUser = (user: object) => localStorage.setItem('user', JSON.stringify(user))
const logout = () => localStorage.clear()
const AuthContext = createContext({
jwt: '',
setJwt: setJwt,
user: {},
setUser: setUser,
loading: false,
setLoading: (loading: boolean) => {},
authenticate: (jwt: string, user: object) => {},
logout: () => {},
})
export const useAuth = () => useContext(AuthContext)
const Auth = ({children}: {children: ReactNode}) => {
const auth = useAuth()
const [jwt, updateJwt] = useState(auth.jwt)
const [user, updateUser] = useState(auth.user)
const [loading, setLoading] = useState(false)
useEffect(() => {
updateJwt(getJwt())
updateUser(getUser())
}, [])
const value = {
jwt: jwt,
setJwt: (jwt: string) => {
setJwt(jwt)
updateJwt(jwt)
},
user: user,
setUser: (user: object) => {
setUser(user)
updateUser(user)
},
loading: loading,
setLoading: setLoading,
authenticate: (jwt: string, user: object) => {
setJwt(jwt)
updateJwt(jwt)
setUser(user)
updateUser(user)
},
logout: () => {
localStorage.removeItem('jwt')
localStorage.removeItem('user')
updateJwt('')
updateUser({})
setLoading(false)
},
}
return <AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
}
export default Auth
...
// app.tsx
import Auth from './auth'
...
<Auth>
<Router/>
</Auth>
// or something like that
...
import {useAuth} from './auth'
// in any component to pull auth from global context state
You can change that according to whatever you need.
I know the issue why its happening but don't know the solution...But i am not fully sure...Look how react works is if any parents re render it also cause re render the children..ok?Its mean if any reason your apps is re rendering and the useAuth keep firing...so for this there to much console log.But i am not sure that it will work or not..give me your repo i will try on my local computer
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus,currentUser } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [currentUser]);
//only fire when currentUser change
...
}
Update your useEffect hook like so:
useEffect(() => {
const unsub = firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
} else {
console.log('User logged out.');
}
});
return ()=> unsub;
},[])

How can I get data from AsyncStorage and add it to initial state when using useReducer?

I'm using useContext & useReducer from Hooks & AsyncStorage.setItem in useEffect to save the data whenever state gets updated. On app reload I want to make sure to get saved data using AsyncStorage.getItem and add it to the initial state.
I tried to add init function with async as the third property to useReducer but still, it's not replacing initial data with received data. Please go through below code and help.
Thank you in advance!
Current code where I can save data to AsyncStorage
const [user, dispatch] = useReducer(userReducer, {});
useEffect(() => {
AsyncStorage.setItem('user', JSON.stringify(user))
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Below is the code I tried, but unable save existing data as initial data.
const getUser = async function() {
const userData = await AsyncStorage.getItem('user')
console.log("parse");
console.log(userData);
console.log("parsed data");
console.log(JSON.parse(userData));
return userData ? JSON.parse(userData) : {};
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [user, dispatch] = useReducer(userReducer, {}, getUser);
useEffect(() => {
console.log("context");
console.log(JSON.stringify(user));
AsyncStorage.setItem('user', JSON.stringify(user))
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Thank you!
Updated and working code based on below suggestion and with minor changes.
const getUser = async () => {
try {
const user = await AsyncStorage.getItem('user')
return user ? JSON.parse(user) : {};
} catch (e) {
console.log('Failed to fetch the data from storage');
}
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [user, dispatch] = useReducer(userReducer, {});
// Loading initial Satte
useEffect(() => {
async function fetchUser() {
const user = await getUser();
dispatch({type: 'ADD_USER', user});
}
fetchUser();
}, []);
// Update AsyncStorage when user is updated
useEffect(() => {
// This check is required to avoid initial writing to asyncStorage
if(user) {
AsyncStorage.setItem('user', JSON.stringify(user))
}
}, [user]);
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}
Your initial state to useReducer needs to synchronous. Since asyncStorage is not a synchronous API you can't actually pass the value as initialState
You however can make use of useEffect loading state like below
const getUser = async () => {
try {
const user = await AsyncStorage.getItem('user')
return user ? JSON.parse(user) : {};
} catch (e) {
console.log('Failed to fetch the data from storage');
}
}
export const UserContext = createContext();
const UserContextProvider = (props) => {
const [isLoading, setIsLoading] = useState(true);
const [user, dispatch] = useReducer(userReducer, {});
// Loading initial Satte
useEffect(() => {
async function fetchUser() {
const user = await getUser();
setIsLoading(false);
dispatch({type: 'INIT_REDUCER', user}); // You must handle initReducer in reducer
}
fetchUser();
}, []);
useEffect(() => {
if(user) {
// This check is required to avoid initial writing to asyncStorage
console.log("context");
console.log(JSON.stringify(user));
AsyncStorage.setItem('user', JSON.stringify(user))
}
}, [user]);
if(isLoading) {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
return(
<UserContext.Provider value={{user,dispatch}}>
{props.children}
</UserContext.Provider>
);
}

Resources