The server is returning user data. In particular, I am interested in his rights. With these rights, I will render the components. I am thinking of passing this data to components and doing some checks. All routes are in the index.js
import {Notfound404} from './components/404/404NotFound'
import {UserContext} from './UserContext'
const Router = () => {
const [user, setUser] = useState(null)
return (
<React.StrictMode>
<UserContext.Provider value={{user, setUser}}>
<CookiesProvider>
<BrowserRouter>
<Switch>
<Route exact path={'/'} component={Auth}/>
<Route exact path={'/settings'} component={Settings}/>
<Route exact path={'/event-logs'} component={EventLog}/>
<Route component={Notfound404} status={404}/>
</Switch>
</BrowserRouter>
</CookiesProvider>
</UserContext.Provider>
</React.StrictMode>
)
}
setting user data in Auth.js
import {UserContext} from '../../UserContext'
export const Auth = () => {
const {user, setUser} = useContext(UserContext)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [token, setToken] = useCookies(['qr-token'])
useEffect(() => {
if (token['qr-token']) window.location.href = '/settings'
}, [token])
const credentials = {
username: username,
password: password
}
const sendCredentials = () => {
axios.post(`http://127.0.0.1:8000/api/v1/auth/`, credentials, {
headers: {
'Content-type': 'application/json',
}
})
.then(resp => {
setToken('qr-token', resp.data.token);
setUser(resp.data)
})
.catch(error => console.log(error.response))
}
return !token['qr-token'] &&
<div className={styles.authContainer}>
<label htmlFor={'username'}>Username</label>
<input id={'username'} type={'text'} placeholder={'username'}
onChange={evt => setUsername(evt.target.value)}
/>
<label htmlFor={'password'}>Password</label>
<input id={'password'} type={'password'} onChange={evt =>
setPassword(evt.target.value)}/>
<button onClick={sendCredentials}>Sign In</button>
</div>
)
}
In the index.js I am getting user data after authorization. But in settings.js use data is null
export const Settings = () => {
const {user, setUser} = useContext(UserContext)
console.log(user) # null
....
}
Probably I am doing something wrong and I am asking for your help. Thanks.
As Saba indicated, the issue is about refreshing the page which clears the context. This is done by these lines:
useEffect(() => {
if (token['qr-token']) window.location.href = '/settings'
}, [token])
Don't use the usual redirect methods in Single Page Applications. Instead, use
const history = useHistory();
useEffect(() => {
if (token['qr-token'])
history.push('/settings');
}, [token])
This won't cause a reload of the page and therefore keeps your context alive.
I think a refresh is happening while redirecting to the setting page. That reload cause the context value to get cleared (simply, because the value of the context is coming from a state and that state gets lost as a result of refresh). To fix this, you can store the auth data data in localStorage and assign the context value to it whenever it gets null.
useContext() provides a dispatch function that has been passed down
from the Store component. The dispatch function accepts two arguments,
a type of action, and a payload for updating the global state. The
useContext() function will then return an updated global state.
const { state, dispatch } = useContext(MyContext);
In your code setUser must be triggered with the parameter contains action type and payload.
For example.
setUser({
type: "SET_USER",
payload: resp.data
})
Related
I'm trying to get some auth experience and I've got React with React Router, I found a custom auth check for routes that I thought looked good, and tried to implement. Basically it would sign the user in, change the auth value to true and be able to call on that auth value from the hook to check.
Here's my codesandbox, my problem is I have to use AWS Cognito for this project so the sign in call has to be from an async function and not through a promise...
Clicking 'sign in check' calls handleLogin and starts the async function in the useAuthHook with signIn, which sets authed to true via an effect hook. That change is reflected by using the 'auth check' button, but when trying to navigate to a protected route the console logs the default values.
Here's the steps;
...
<Button variant="primary" onClick={handleLogin} type="button">
sign in check
</Button>
...
const handleLogin = () => {
signIn();
};
Now the signIn hook from useAuthHook;
async signIn() {
const user = "totally real"; //AWS await request
testValue = "a diff string";
if (user) {
setUser(user);
console.log(authed, user);
return "/storageSolution";
}
}
the effect hook that updates a ref hook and a useState hook(testing both cases);
React.useEffect(() => {
if (user) {
authed.current = true;
setStateAuth(true);
}
}, [user]);
both stateAuth and the authed ref are returned along with the earlier signIn, and used in RequireAuth before my routes;
export function RequireAuth({ children }) {
const location = useLocation();
const { authed, user, stateAuth } = useAuth();
console.log(authed, user, stateAuth);
return authed === true ? (
children
) : (
<Navigate to="/" replace state={{ path: location.pathname }} />
);
}
but clicking protected route check after signing in shows default values in the console, whereas clicking auth check shows the updates values.
I found https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function this on stale state, but neither of the reasons it gives seem to be the problem. I've got several different ways of updating and reading that value, but none work. What am I missing?
React hooks don't share state. Move all the state and logic from the useAuth hook into the AuthProvider component. After this is done the useAuth hook simply returns the current authContext value.
Example:
const authContext = React.createContext();
export function useAuth() {
return React.useContext(authContext);
}
export function AuthProvider({ children }) {
const authed = React.useRef(false);
const [user, setUser] = React.useState();
const [stateAuth, setStateAuth] = React.useState(false);
let testValue = "some string";
React.useEffect(() => {
if (user) {
authed.current = true;
setStateAuth(true);
}
}, [user]);
return (
<authContext.Provider
value={{
authed,
user,
testValue,
stateAuth,
async signIn() {
const user = "totally real"; // where the await auth req would be
testValue = "a diff string";
if (user) {
setUser(user);
console.log(authed, user);
return "/storageSolution";
}
}
}}
>
{children}
</authContext.Provider>
);
}
index.js
Import and wrap the app with the AuthProvider component.
...
import { RequireAuth, AuthProvider } from "./useAuthHook";
...
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<LoginSignup />} />
<Route
path="/storageSolution"
element={
<RequireAuth>
<StorageSolution />
</RequireAuth>
}
/>
<Route path="/secret" element={<SecretRoute />} />
</Routes>
</BrowserRouter>
</AuthProvider>
</React.StrictMode>,
rootElement
);
I have a userContext like this :
import React, { useContext, useEffect, useState } from "react";
import * as userService from "./services/appService";
const UserContext = React.createContext();
export function useUserContext() {
return useContext(UserContext);
}
export function UserProvider({ children }) {
const [user, setUser] = useState();
useEffect(() => {
const fetchData = async () => {
const { data } = await userService.allDetails();
setUser(data);
};
fetchData();
}, []);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
I'm wrapping routes with User Provider like this
<UserProvider>
<Route path="/signup" component={Signup} />\
<Route path="/login" component={Login} />\
<ProtectedRoute path="/resetpassword" component={Reset} />
<ProtectedRoute path="/settings" component={Settings} />
<UserProvider>
I'm trying to access it inside a functional component like this:
function Settings(){
const user = useUserContext();
const id = user.id
useEffect(() => {
console.log(id)
},[])
return (
.......
)
}
I'm getting user as undefined in Settings component.
In the UserProvider component, you defined a state user with initial value of undefined, because you did not pass any value to useState. Even though you are making an API call to update the state of user state, but keep in mind that fetching is an asynchronous operation and may take time to finish, which is why when you try to access user context in Settings component, the value is still undefined. You could add an if statement check here to see if user context is truthy or not and use it if it's truthy only, which means that the fetch finished and user state has been updated
Since the user is only loaded as an Effect after the first render of UserProvider, it is still undefined during the first render.
I suggest you suspend rendering the UserProvider and its contents until the fetch completed:
export function UserProvider({ children }) {
const [user, setUser] = useState();
useEffect(() => {
userService.allDetails()
.then(({ data }) => setUser(data));
}, []);
return !user ? null : (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}
I am using Protected Route for my dashboard and I only want people who signed in would be able to see the dashboard. So, in my Login component, I am fetching the data and this is the place where I check if the email and password of the user is right. So what I want to do is send a boolean variable to parent element which is index.js and according to the value I want to show the dashboard to the user.
So here is my index.js:
const hist = createBrowserHistory();
console.log(hist)
ReactDOM.render(
<Router history={hist}>
<Switch>
<Route path="/" component={Login} exact />
<ProtectedRoute path="/admin" component={(props) => <AdminLayout {...props} />} isAuth={true}/>
<Route path="" component={() => "404 NOT FOUND"} />
</Switch>
</Router>,
document.getElementById("root")
);
So instead of the true inside, isAuth={true}, I want to send a variable to check.
Also my Login component:
const Login = ({ submitForm }) => {
const [isSubmitted, setIsSubmitted] = useState(false);
function submitForm() {
setIsSubmitted(true);
}
const { handleChange, values, handleSubmit, errors } = useForm(
submitForm,
validate
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [login, setLogin] = useState(false);
const fetchLogin = async (email, password) => {
try {
setLoading(true);
const res = await Axios({
data: {
user_email: email,
user_password: password,
},
});
if (res.status === 200) {
setLogin(true);
localStorage.setItem("user-info", JSON.stringify(res));
}
setLoading(false);
} catch (err) {
setError(err.message);
setLoading(false);
}
};
function loginButton() {
fetchLogin(values.email, values.password);
}
return (
<form>
</form>
);
};
export default Login;
So thanks for your helps...
There are different ways.
You can read retrieve user-info from localStroage in your ProtectedRoute. In this case Login would not need to do anything more than localStorage.setItem("user-info", JSON.stringify(res))
You can propagate the login-state up to the parent component using a callback function. See this question for an example.
If you want to access the login state also from other components you could consider to use a React Context. Here is an example.
I'm using code from a tutorial, which uses createContext and I'm kind of confused on what exactly it's doing, and I believe that it's causing errors where I wouldn't necessarily expect. I have two components, Dashboard and Login which are different pages of my web app. It generates the error: Unhandled Rejection (TypeError): Cannot read property 'data' of undefined For some reason, the following line in Dashboard.js:
function Dashboard() {
const [favPokemons, setFavPokemons] = useState([]);
const { userData, setUserData } = useContext(UserContext);
setFavPokemons(userData.user.favPokemon); // This line is the problematic line
}
causes an error in Login.js in its try catch clause:
import UserContext from "../../context/userContext";
import ErrorNotice from "../misc/ErrorNotice";
function Login () {
const [email, setEmail] = useState();
const [password, setPassword] = useState();
const [error, setError] = useState();
const { setUserData } = useContext(UserContext);
const history = useHistory();
const submit = async (e) => {
e.preventDefault();
try{
const loginUser = {email, password};
const loginResponse = await axios.post("https://minipokedexbackend.herokuapp.com/users/login", loginUser);
console.log(userData); // line Login.js:21 is in image below line 22
console.log(loginResponse) // line Login.js:22, log is in image below
setUserData({
token: loginResponse.data.token,
user: loginResponse.data.user
});
localStorage.setItem("auth-token", loginResponse.data.token);
history.push("/dashboard");
} catch(err) {
err.response.data.msg && setError(err.response.data.msg)
}
};
Could someone explain what createContext and why it would be causing an error in two seemingly unrelated components? I have a feeling that it has to do with userData not quite being generated when Dashboard is rendered?
EDIT:
Sorry for the lack of information, data referenced in the Login.js file is data from my server accessing mongoDB. Its response contains token and user info, which includes their id, displayname and an array of favpokemon
Here's userContext.js:
import { createContext } from 'react';
export default createContext(null);
Here's App.js:
function App() {
const [ userData, setUserData] = useState({
token: undefined,
user: undefined
});
return (
<BrowserRouter>
<UserContext.Provider value={{ userData, setUserData }}>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/register" component={Register} />
<Route path="/login" component={Login} />
<Route path='/dashboard' component={Dashboard}/>
</Switch>
</UserContext.Provider>
</BrowserRouter>
);
}
App.js also contains some functions to check if the user is logged in.
With the info you provided in the question, I managed to create a structure of your code. There might be logic or syntax errors because of the lack of information, but I want you to have a general idea how to use Context Hook during login.
UserContext.tsx
import React, { createContext, useState } from "react";
//create the context
export const UserContext = createContext<any>(undefined);
//create the context provider, we are using useState to ensure that we get reactive values from the context
export const UserProvider: React.FC = ({ children }) => {
//the reactive values
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [userData, setUserData] =setUserData({
token: '',
user: ''
});
//the store object
let state = {
email,
setEmail,
password,
setPassword,
userData.
setUserData
};
//wrap the application in the provider with the initialized context
return <UserContext.Provider value={state}>{children}</UserContext.Provider>;
};
export default UserContext;
Login.tsx
import UserContext from "../../context/userContext";
import ErrorNotice from "../misc/ErrorNotice";
function Login () {
const [error, setError] = useState();
const { email, setEmail, password, setPassword, userData, setUserData } = useContext(UserContext);
const history = useHistory();
const submit = async (e) => {
e.preventDefault();
try{
const loginUser = {email, password};
const loginResponse = await axios.post("https://minipokedexbackend.herokuapp.com/users/login", loginUser);
console.log(userData); // line Login.js:21 is in image below line 22
console.log(loginResponse) // line Login.js:22, log is in image below
setUserData({
token: loginResponse.data.token,
user: loginResponse.data.user
});
localStorage.setItem("auth-token", loginResponse.data.token);
history.push("/dashboard");
} catch(err) {
err.response.data.msg && setError(err.response.data.msg)
}
};
export default Login;
Dashboard.tsx
import UserContext from "../../context/userContext";
import ErrorNotice from "../misc/ErrorNotice";
import React, { useState, useEffect } from "react";
const Dashboard: React.FC = () => {
const [favPokemons, setFavPokemons] = useState([]);
const { userData, setUserData } = useContext(UserContext);
useEffect(() => {
setFavPokemons(userData?.user?.favPokemon);
}, []);
}
}
export default Dashboard;
App.tsx
Here I'm using a ternary expression. If (userData.token), got to dashboard otherwise go to Login Page.
function App() {
const { userData} = useContext(UserContext);
return (
<UserProvider>
<BrowserRouter>
<Header />
<Switch>
{!userData?.token ? (
<>
<Route path="/register" component={Register} />
<Route path="/login" component={Login} />
<Redirect exact from="/" to="/login" />
</>
) : (
<>
<Route exact path="/" component={Home} />
<Route path='/dashboard' component={Dashboard}/>
<Redirect exact from="/" to="/dashboard" />
</>
)
</Switch>
</BrowserRouter>
</UserProvider>
);
}
Try to make a habit of using null checks when you’re accessing nested values of a response.
setFavPokemons(userData.user.favPokemon);
Here, modify this line to:
setFavPokemons(userData?.user?.favPokemon);
Also, do you mind doing a console log on userData object or share the corresponding reducer to check whether the shape of the data is same or not?
I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;