I have an app using ReactJS and Parse Server. But the data that I'm passing on from the API is not setting after refreshing the page. Here's my code
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import Parse from 'parse';
const AuthContext = React.createContext({});
const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoggingIn, setLoginLoading] = useState(null);
const [loginError, setLoginError] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(Parse.User.current());
const setUserSession = currentUser => {
setIsAuthenticated(currentUser);
setLoginLoading(false);
setLoginError(null);
};
const handleLogin = async values => {
setLoginLoading(true);
setLoginError(null);
setSessionError(false);
try {
const res = await Parse.Cloud.run('login', values);
setUser(res); // THIS IS THE DATA I WANT TO GET FROM THE API
Parse.User.become(res.session).then(setUserSession, () => {
setLoginError(TOKEN_VERIFY_ERROR);
setLoginLoading(false);
});
setLoginLoading(false);
} catch (e) {
setLoginLoading(false);
setLoginError(e.message);
}
};
};
return (
<AuthContext.Provider
value={{
user,
login: handleLogin,
isLoggingIn,
isAuthenticated,
loginError,
}}
>
{children}
</AuthContext.Provider>
);
const AuthProvider = withRouter(AuthContextProvider);
export { AuthContext, AuthProvider };
AuthContextProvider.propTypes = {
children: PropTypes.node,
};
upon logging in, user are there but when I refresh it, it returns null. What should I do to retain the details? I'm avoiding setting this to localStorage.
Related
Working on a nextjs app using typescript w/ a firestore backend.
I currently am using my own useContext hook to make globally available a firestore user object, an array of 'business entity IDs' (strings) that the user is authorized to view, as well as an array of objects containing each biz's document data.
Because a user can be a member of multiple business entities, I'd like to be able to have them toggle between a 'current business' object whose document ID would be used to pass into further queries as well as document add / update functions.
I'm just not sure how to go about trigger the setting and switching of this 'current biz' id.
I'm assuming I'd want to somehow set the value in another usecontext hook so that the value is globally available to use around the app?
Below is my current custom context hooks doc
please let me know if you'd need to see any more data
import { auth, firestore } from "../lib/firebase";
import { useContext, useEffect, useState } from "react";
import { useAuthState } from "react-firebase-hooks/auth";
import {
doc,
onSnapshot,
collection,
query,
where,
getDocs,
} from "firebase/firestore";
import { BizContext } from "./context";
//types
// Custom hook to read auth record and user profile doc
export function useUserData() {
const [user, loading, error] = useAuthState(auth);
const [username, setUsername] = useState(null);
useEffect(() => {
// turn off realtime subscription
let unsubscribe;
//this actively listens to a the users collection
if (user) {
const userDocRef = doc(firestore, "users", user.uid);
unsubscribe = onSnapshot(userDocRef, (doc) => {
setUsername(doc.data()?.username);
});
} else {
setUsername(null);
}
return unsubscribe;
}, [user]);
return { user, username };
}
export function useBelongsTo() {
const [user] = useAuthState(auth);
const [belongsTo, setBelongsTo] = useState<[]>([]);
const [bizInfo, setBizInfo] = useState<[]>([]);
useEffect(() => {
let unsubscribe;
if (user) {
const bizColRef = collection(firestore, "businesses");
const q = query(bizColRef, where("admins", "array-contains", user.uid));
unsubscribe = onSnapshot(q, (querySnapshot) => {
let bizId: [] = [];
let bizInfo: [] = [];
querySnapshot.forEach((doc) => {
bizId.push(doc.id);
bizInfo.push(doc.data());
});
setBelongsTo(bizId);
setBizInfo(bizInfo);
});
} else {
setBelongsTo([]);
console.log("no businesses, no user");
}
return unsubscribe;
}, [user]);
return { belongsTo, bizInfo };
}
_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { ChakraProvider } from "#chakra-ui/react";
import customTheme from "../extendTheme";
import { Toaster } from "react-hot-toast";
import { BizContext, UserContext } from "../lib/context";
import { useBelongsTo, useUserData } from "../lib/hooks";
import Layout from "../components/layout/Layout";
function MyApp({ Component, pageProps }: AppProps) {
const userData = useUserData();
const bizData = useBelongsTo();
return (
<ChakraProvider theme={customTheme}>
<UserContext.Provider value={userData}>
<BizContext.Provider value={bizData}>
<Layout>
<Component {...pageProps} />
<Toaster />
</Layout>
</BizContext.Provider>
</UserContext.Provider>
</ChakraProvider>
);
}
export default MyApp;
context.tsx
import React from "react";
export const UserContext = React.createContext({});
export const BizContext = React.createContext({});
If you want to make belongsTo and bizInfo globally for later usage, I recommend you to use react Context instead of using hooks. Move the logic to the context like the sample below.
import { useContext, createContext, useEffect, useState } from "react";
export const AuthContext = createContext();
export function AuthContextProvider({ children }) {
const [user] = useAuthState(auth);
const [belongsTo, setBelongsTo] = useState<[]>([]);
const [bizInfo, setBizInfo] = useState<[]>([]);
const [currentBizId, setCurrentBizId] = useState();
useEffect(() => {
let unsubscribe;
if (user) {
const bizColRef = collection(firestore, "businesses");
const q = query(bizColRef, where("admins", "array-contains", user.uid));
unsubscribe = onSnapshot(q, (querySnapshot) => {
let bizId: [] = [];
let bizInfo: [] = [];
querySnapshot.forEach((doc) => {
bizId.push(doc.id);
bizInfo.push(doc.data());
});
setBelongsTo(bizId);
setBizInfo(bizInfo);
});
} else {
setBelongsTo([]);
console.log("no businesses, no user");
}
return unsubscribe;
}, [user]);
function switchBusinessEntity(id){
let currentBusinessEntity=belongsTo.find(biz=>biz.id === id);
setCurrentBizId(currentBusinessEntity);
}
return <AuthContext.Provider
value={{
belongsTo,
bizInfo,
currentBizId,
switchBusinessEntity
}}>
{children}
</AuthContext.Provider>;
}
export const useAuthContext = () => useContext(AuthContext);
And in the _app.js, wrapper the App with AuthContextProvider
import { AuthContextProvider } from "#/contexts/authContext";
function MyApp({ Component, pageProps }) {
return <AuthContextProvider>
<Component {...pageProps} />
</AuthContextProvider>;
}
export default MyApp;
And in the component, you can access the global states like this:
function YourComponent(){
const {
belongsTo,
bizInfo,
currentBizId,
switchBusinessEntity
} = useAuthContext()
return <select onChange={e => switchBusinessEntity(e.target.value)>
{belongsTo.map((bizId, index) => <option key={index} value={bizId.value}>{bizId}</option>)}
</select>
}
I have a problem with flickering my private routes while using my AuthContext. Below is the code for my Private Route:
import React from 'react';
import { Navigate } from 'react-router-dom';
import { UserAuth } from '../../Context/AuthContext';
const PrivateRoute = ({ children }) => {
const { user } = UserAuth();
if (!user) {
return <Navigate to='/login' />;
}
return children;
};
export default PrivateRoute;
No personal information shows, because the user is initialized to {} in Auth Context. but I can still see the page and navbar. Anyone have a solution?
Also, below is AuthContext.js:
import { createContext, useContext, useEffect, useState } from 'react';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from 'firebase/auth';
import { auth } from '../../firebase';
const UserContext = createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({});
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
};
const signIn = (email, password) => {
return signInWithEmailAndPassword(auth, email, password)
}
const logout = () => {
return signOut(auth)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
//console.log(currentUser);
setUser(currentUser);
});
return () => {
unsubscribe();
};
}, []);
return (
<UserContext.Provider value={{ createUser, user, logout, signIn }}>
{children}
</UserContext.Provider>
);
};
export const UserAuth = () => {
return useContext(UserContext);
};
So I found a solution that's kind of cheeky. I'm not going to post my solution, but basically, I wrapped the return statement return children in the PrivateRoute function with an if statement for a specific item in the user object. This prevents any return and 'solves' the flicker.
I'll receive the following error:
TypeError: Cannot destructure property 'isAuthenticated' of 'Object(...)(...)' as it is null. When I try to use a function from a custom hook inside my AuthContext.
I have a function written in a custom hook (useWeb3.ts), like:
import { useContext } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import Web3 from "web3";
import { AuthContext } from "../context/AuthContext";
import { FormatPublicKey } from "./utils/FormatPublicKey";
const useWeb3 = () => {
const { isAuthenticated, setIsAuthenticated, web3, setWeb3, setUserInfo } = useContext(AuthContext);
const getCurrentProvider = () => {
let provider;
if (window.ethereum) {
provider = window.ethereum;
} else if (window.web3) {
provider = window.web3.currentProvider;
} else {
console.log("Non-Ethereum browser detected. You should consider trying MetaMask!");
}
return provider;
};
return {
getCurrentProvider,
};
};
export default useWeb3;
And I want to call this function (getCurrentProvider) inside my context file, like:
import { createContext, useState } from "react";
import useWeb3 from "../components/useWeb3";
import { UserInfoProps } from "../types/UserInfoProps";
export const AuthContext = createContext<any>(null);
export const AuthContextProvider = (props: any) => {
const initializeState = JSON.parse(localStorage.getItem("userAccount")!);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [isAuthenticating, setIsAuthenticating] = useState<boolean>(false);
const [authError, setAuthError] = useState<string>("");
const [provider, setProvider] = useState(null);
const [web3, setWeb3] = useState(null);
const [userInfo, setUserInfo] = useState<UserInfoProps | null>(initializeState || null);
const { getCurrentProvider } = useWeb3();
const providerValue = {
isAuthenticated,
setIsAuthenticated,
isAuthenticating,
setIsAuthenticating,
authError,
setAuthError,
provider,
setProvider,
web3,
setWeb3,
userInfo,
setUserInfo
};
return (
<AuthContext.Provider value={providerValue}>
{props.children}
</AuthContext.Provider>
);
};
The AuthContext is wrapped around App inside my index, see:
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);
I already read some similar problems and that was solved by changing some imports/exports. But somehow I still receive the error. The error does come up as soon as I call this line of code : const { getCurrentProvider } = useWeb3(); inside the AuthContext.tsx
Thanks in advance
When a user log to a react app, I fill data to authState object. Inside the app I fill other state objects with data. I want to clear all those states when the user logout
for example I have this provider
import { createContext, useEffect, useReducer } from "react";
import auth from "./reducers/auth";
import pendiente from "./reducers/pendiente";
import historico from "./reducers/historico";
import authInitialState from "./initialStates/authInitialState";
import pendienteInitialState from "./initialStates/pendienteInitialState";
import historicoInitialState from "./initialStates/historicoInitialState";
export const GlobalContext = createContext();
export const GlobalProvider = ({ children }) => {
const [authState, authDispatch] = useReducer(auth, [], () => {
const localData = localStorage.auth;
return localData ? JSON.parse(localData): authInitialState;
});
const [pendienteState, pendienteDispatch] = useReducer(
pendiente,
pendienteInitialState
);
const [historicoState, historicoDispatch] = useReducer(
historico,
historicoInitialState
);
useEffect(() => {
localStorage.auth = JSON.stringify(authState);
}, [authState]);
return (
<GlobalContext.Provider
value={{
authState,
authDispatch,
pendienteState,
pendienteDispatch,
historicoState,
historicoDispatch,
}}
>
{children}
</GlobalContext.Provider>
);
};
In Logout function I'm sending and action (logout) with 3 dispatchs.
const {
authState,
authDispatch,
pendienteDispatch,
historicoDispatch,
} = useContext(GlobalContext);
const handleLogout = () => {
logout(history)(authDispatch, pendienteDispatch, historicoDispatch);
};
Inside the action I send a dispatch an to every sate objcet to clear the data with it's initial state
This works fine, but I think this is not the correct way to do it
const logout = (history) => (
dispatch,
pendienteDispatch,
historicoDispatch
) => {
localStorage.removeItem("token");
dispatch({ type: LOGOUT_USER });
pendienteDispatch({ type: CLEAR_PENDIENTE_DATA });
historicoDispatch({ type: CLEAR_HISTORICO_DATA });
history.push("/");
};
¿Any ideas ?
I have a simple Dashboard component that relies on React context to manage auth. It contains a custom hook useAuth to extract the current user as well as the auth related functions: login, logout, etc.
This is the Context file: AuthContext.js:
import React, { createContext, useContext, useState, useEffect } from "react";
import { auth } from "../config/firebase";
const AuthContext = 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);
}
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
function logout() {
return auth.signOut();
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
currentUser,
signup,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
This is the Dashboard.js component:
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
export default function Dashboard() {
const { currentUser, logout } = useAuth();
const [error, setError] = useState("");
const history = useHistory();
const handleLogout = async () => {
setError("");
try {
await logout();
history.push("/login");
} catch (e) {
setError(e.message);
}
};
return (
<div>
{error && <p>{error}</p>}
<h1>This is the Dashboard</h1>
<h5>Email: {currentUser.email}</h5>
<button onClick={handleLogout} type="button">
Logout
</button>
</div>
);
}
As recommened by React Testing Library I have created a test-utils.js file:
import React, { createContext } from "react";
import { render } from "#testing-library/react";
import { BrowserRouter as Router } from "react-router-dom";
const AuthContext = createContext();
const currentUser = {
email: "abc#abc.com",
};
const signup = jest.fn();
const login = jest.fn();
const logout = jest.fn();
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};
const customRender = (ui, options) => {
render(ui, { wrapper: AllTheProviders, ...options });
};
export * from "#testing-library/react";
export { customRender as render };
However, when running Dashboard.test.js I get error
TypeError: Cannot destructure property 'currentUser' of '((cov_5mwatn2cf(...).s[0]++) , (0 , _AuthContext.useAuth)(...))' as it is undefined.
4 |
5 | export default function Dashboard() {
> 6 | const { currentUser, logout } = useAuth();
| ^
7 | const [error, setError] = useState("");
8 | const history = useHistory();
import React from "react";
import Dashboard from "./Dashboard";
import { act, render, screen } from "../config/test-utils-dva";
beforeEach(async () => {
await act(async () => {
render(<Dashboard />);
});
});
test("displays dashboard", () => {
expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
});
I think it is because Dashboard component is trying to use useAuth from AuthContext.js, how can I force the rendered Dashboard component to use the mocked data that I am sending in the test-utils.jsfile?
Instead of creating a new context, use the AuthContext from context/AuthContext for <AuthContext.Provider>, as that's the context that the hook uses.
So, in AuthContext.js, export the context instance:
export const AuthContext = createContext();
Then, in your test-util.js file, instead of again calling createContext (which will create a completely separate context instance - the contexts are not the same even if they are stored in a variable with the same name!), just import the previously exported instance:
import { AuthContext } from "../context/AuthContext";
const AllTheProviders = ({ children }) => {
return (
<Router>
<AuthContext.Provider value={{ currentUser, signup, login, logout }}>
{children}
</AuthContext.Provider>
</Router>
);
};