Reset context from helper service - reactjs

I have created a service class that I use to handle authentication in my React app. Data about the current logged in user is stored in the user context. When a user clicks the logout button, logout() is called in the authencation service. The user context can then be reset in the same click event with this.context.updateUser().
But, there are times where a user needs to be logged out even if they didn't click the logout button, for example if their session is invalid. In this case forceLogout() would be called from inside the authentication service. However, because this does not occur within a component this leaves me with no way to reset the user context. The current code gives the error: Error: Invalid hook call. Hooks can only be called inside of the body of a function component..
How can I reset the user context from inside forceLogout()?
// user.context.js
import React, { createContext, Component } from 'react';
import AuthService from '../services/auth.service';
export const UserContext = createContext();
class Context extends Component {
state = {
user: AuthService.getCurrentUser(),
updateUser: (user) => {
this.setState({ user });
}
};
render() {
return (
<UserContext.Provider value={this.state}>
{this.props.children}
</UserContext.Provider>
);
}
}
export default Context;
// auth.service.js
import { useContext } from 'react';
import { UserContext } from '../contexts/user.context';
import history from '../history';
Class AuthService {
getCurrentUser() {
return JSON.parse(localStorage.getItem('user'));
}
logout() {
localStorage.removeItem('user');
}
forceLogout() {
const userContext = useContext(UserContext);
localStorage.removeItem('user');
userContext.updateUser();
history.push('/');
}
...
}

Create a Logout component that will be common for all types of logout, and apply your logout logic inside componentDidMount or useEffect of the Logout component, since it is a component you can use hooks here, and whenever you want to logout just redirect to the Logout component and if you want to redirect back to the homepage after logout do this inside Logout component.

You can use custom events:
Class AuthService {
// ...
forceLogout() {
localStorage.removeItem('user');
// Dispatch logout event.
const event = new Event('force-logout');
window.dispatchEvent(event);
}
}
Then inside a component under UserContext.Provider :
const SomeComponent = () => {
const userContext = useContext(UserContext);
useEffect(() => {
const handleLogout = () => {
userContext.updateUser();
history.push('/');
};
window.addEventListener('force-logout', handleLogout);
return () => window.removeEventListener('force-logout', handleLogout);
}, [userContext, history]);
// ...
}

Related

React Navigate to page based on useEffect dependency

I'm using GoTrue-JS to authenticate users on a Gatsby site I'm working on and I want the homepage to route users to either their user homepage or back to the login page.
I check the existence of a logged-in user in a Context layer then define a state (user) that is evaluated on the homepage with a useEffect hook with the state as the dependency.
The expected behavior is that the useEffect hook will trigger the check for a user once the function is completed and route the user. But what happens is that the hook seems to check without the user state getting changed which routes the user to the login page.
Here's an abridged version of the code:
context.js
import React, {
useEffect,
createContext,
useState,
useCallback,
useMemo,
} from "react";
import GoTrue from 'gotrue-js';
export const IdentityContext = createContext();
const IdentityContextProvider = (props) => {
//create the user state
const [user, setUser] = useState(null);
//init GoTrue-JS
const auth = useMemo(() => {
return new GoTrue({
APIUrl: "https://XXXXXX.netlify.app/.netlify/identity",
audience: "",
setCookie: true,
});
},[]);
//get the user if they are signed in
useEffect(() => {
setUser(auth.currentUser());
},[auth]);
return (
<IdentityContext.Provider value={{ auth,user }}>
{props.children}
</IdentityContext.Provider>
)
}
export default IdentityContextProvider;
index.js
import { navigate } from 'gatsby-link';
import { useContext, useEffect } from 'react'
import { IdentityContext } from '../contexts/IdentityContext';
export default function HomePage() {
const { user } = useContext(IdentityContext);
useEffect(() => {
if (user) {
navigate("/user/home");
console.log("there's a user");
} else {
navigate("/login");
console.log("no user");
}
}, [user]);
return null
}
When I remove the navigate functions I see no user, then there's a user in the log when I load the homepage. I thought the useEffect hook would only fire if the state I listed in the dependency array (user) was changed. If there's no user then auth.currentUser() will return null and if there is one, then I will get all the user data.
Why are you using the user as a dependency? Just use the useEffect with an empty dependency. Also if you want to block the view while the processing, make a state as isLoading ( bool) and conditional render with it
!isLoading ?
<></>
:
<h1>Loading..</h1>
Here's the solution: Netlify's gotrue-js will return null for currentUser() if there is no user signed in so I need to first declare my user state as something other than null then set my conditional to detect null specifically so my app knows the check for a signed in user occurred.
context.js
import React, {
useEffect,
createContext,
useState,
useCallback,
useMemo,
} from "react";
import GoTrue from 'gotrue-js';
export const IdentityContext = createContext();
const IdentityContextProvider = (props) => {
//create the user state
//set either to empty string or undefined
const [user, setUser] = useState("");
//init GoTrue-JS
const auth = useMemo(() => {
return new GoTrue({
APIUrl: "https://XXXXXX.netlify.app/.netlify/identity",
audience: "",
setCookie: true,
});
},[]);
//get the user if they are signed in
useEffect(() => {
setUser(auth.currentUser());
},[auth]);
return (
<IdentityContext.Provider value={{ auth,user }}>
{props.children}
</IdentityContext.Provider>
)
}
export default IdentityContextProvider;
index.js
import { navigate } from 'gatsby-link';
import { useContext, useEffect } from 'react'
import { IdentityContext } from '../contexts/IdentityContext';
export default function HomePage() {
const { user } = useContext(IdentityContext);
useEffect(() => {
if (user) {
navigate("/user/home");
console.log("there's a user");
} else if (user == null) {
navigate("/login");
console.log("no user");
}
}, [user]);
return null
}
There was a similar question regarding Firebase where they were also getting no user on load even when one was signed in because of the state. The accepted answer is doesn't provide a snippet so it's gone to the wisdom of the ancients, but I was able to work with another engineer to get this solution.

firebase onAuthStateChanged executes while sign-in state doesn't change, it also returns null before returning user

I'm learning firebase authentication in react. I believed onAuthStateChanged only triggers when the user sign-in state changes. But even when I go to a different route or refresh the page, it would still execute.
Here is my AuthContext.js
import React, {useContext,useEffect,useState} from 'react';
import {auth} from './firebase';
import { createUserWithEmailAndPassword, onAuthStateChanged, signInWithEmailAndPassword,
signOut } from "firebase/auth";
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState();
const [loading,setLoading] = useState(true);
useEffect(()=>{
const unsub = onAuthStateChanged(auth,user=>{
setLoading(false);
setCurrentUser(user);
console.log("Auth state changed");
})
return unsub;
},[])
function signUp(email,password){
return createUserWithEmailAndPassword(auth,email,password)
}
function login(email,password){
return signInWithEmailAndPassword(auth,email,password);
}
function logout(){
return signOut(auth);
}
const values = {
currentUser,
signUp,
login,
logout
}
return <AuthContext.Provider value={values}>
{!loading && children}
</AuthContext.Provider>;
}
I put onAuthStateChanged in useEffect(), so every time the component renders the code inside will run. But why would onAuthStateChanged() still run when user sign-in state does not change? I'm asking this question because it created problems.
onAuthStateChanged would first return a user of null. If user is already authenticated, it would return the user a second time. Because of the first "null" user, my other page would not work properly. For example, I have this private router that would always redirect to the login page even when the user is authenticated.
My Private Route
import React from 'react';
import {Route, Navigate} from "react-router-dom";
import { useAuth } from './AuthContext';
export default function Private() {
const {currentUser} = useAuth();
return <div>
{currentUser ? <>something</>:<Navigate to="/login"/>}
</div>;
}
if onAuthStateChanged doesn't trigger when I don't sign in or log out, I wouldn't have the problems mentioned above
I don't think the issue is with onAuthStateChange per se, but rather the fact that you're setting loading to false first, and only setting the current user afterwards. While react attempts to batch multiple set states together, asyncronous callbacks which react is unaware of won't be automatically batched (not until react 18 anyway).
So you set loading to false, and the component rerenders with loading === false, and currentUser is still on its initial value of undefined. This then renders a <Navigate>, redirecting you. A moment later, you set the currentUser, but the redirect has already happened.
Try using unstable_batchedUpdates to tell react to combine the state changes into one update:
import React, { useContext, useEffect, useState } from "react";
import { unstable_batchedUpdates } from 'react-dom';
// ...
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => {
unstable_batchedUpdates(() => {
setLoading(false);
setCurrentUser(user);
});
console.log("Auth state changed");
});
return unsub;
}, []);

I wonder if this really is the correct way to use onAuthStateChanged

Following this react-firestore-tutorial
and the GitHub code. I wonder if the following is correct way to use the onAuthStateChanged or if I have understod this incorrect I'm just confused if this is the right way.
CodeSandBox fully connect with a test-account with apikey to Firebase!! so you can try it what I mean and I can learn this.
(NOTE: Firebase is blocking Codesandbox url even it's in Authorised domains, sorry about that but you can still see the code)
t {code: "auth/too-many-requests", message: "We have blocked all
requests from this device due to unusual activity. Try again later.",
a: null}a:
Note this is a Reactjs-Vanilla fully fledge advanced website using only;
React 16.6
React Router 5
Firebase 7
Here in the code the Firebase.js have this onAuthStateChanged and its called from two different components and also multiple times and what I understand one should only set it up once and then listen for it's callback. Calling it multiple times will that not create many listeners?
Can someone have a look at this code is this normal in Reactjs to handle onAuthStateChanged?
(src\components\Firebase\firebase.js)
import app from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
class Firebase {
constructor() {
app.initializeApp(config);
.......
}
.....
onAuthUserListener = (next, fallback) =>
this.auth.onAuthStateChanged(authUser => {
if (authUser) {
this.user(authUser.uid)
.get()
.then(snapshot => {
const dbUser = snapshot.data();
// default empty roles
if (!dbUser.roles) {
dbUser.roles = {};
}
// merge auth and db user
authUser = {
uid: authUser.uid,
email: authUser.email,
emailVerified: authUser.emailVerified,
providerData: authUser.providerData,
...dbUser,
};
next(authUser);
});
} else {
fallback();
}
});
user = uid => this.db.doc(`users/${uid}`);
}
export default Firebase;
This two rect-higher-order Components:
First withAuthentication:
(src\components\Session\withAuthentication.js)
import React from 'react';
import AuthUserContext from './context';
import { withFirebase } from '../Firebase';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
localStorage.setItem('authUser', JSON.stringify(authUser));
this.setState({ authUser });
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
},
);
}
componentWillUnmount() {
this.listener();
}
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withFirebase(WithAuthentication);
};
export default withAuthentication;
And withAuthorization:
(src\components\Session\withAuthorization.js)
import React from 'react';
import { withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import AuthUserContext from './context';
import { withFirebase } from '../Firebase';
import * as ROUTES from '../../constants/routes';
const withAuthorization = condition => Component => {
class WithAuthorization extends React.Component {
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
if (!condition(authUser)) {
this.props.history.push(ROUTES.SIGN_IN);
}
},
() => this.props.history.push(ROUTES.SIGN_IN),
);
}
componentWillUnmount() {
this.listener();
}
render() {
return (
<AuthUserContext.Consumer>
{authUser =>
condition(authUser) ? <Component {...this.props} /> : null
}
</AuthUserContext.Consumer>
);
}
}
return compose(
withRouter,
withFirebase,
)(WithAuthorization);
};
export default withAuthorization;
This is normal. onAuthStateChanged receives an observer function to which a user object is passed if sign-in is successful, else not.
Author has wrapped onAuthStateChanged with a higher order function – onAuthUserListener. The HOF receives two parameters as functions, next and fallback. These two parameters are the sole difference when creating HOC's withAuthentication and withAuthorization.
The former's next parameter is a function which stores user data on localStorage
localStorage.setItem('authUser', JSON.stringify(authUser));
this.setState({ authUser });
while the latter's next parameter redirects to a new route based on condition.
if (!condition(authUser)) {
this.props.history.push(ROUTES.SIGN_IN);
}
So, we are just passing different observer function based on different requirements. The component's we will be wrapping our HOC with will get their respective observer function on instantiation. The observer function are serving different functionality based on the auth state change event. Hence, to answer your question, it's completely valid.
Reference:
https://firebase.google.com/docs/reference/js/firebase.auth.Auth#onauthstatechanged
https://reactjs.org/docs/higher-order-components.html

How to architect handling onSuccess of a redux dispatched request that becomes a React Navigation change of screen

I have a Registration screen.
The result of a successful registration will update the account store with the state:
{error: null, token: "acme-auth" ...}
On the Registration screen I render an error if there is one from the store.
What I want to do is navigate to the Dashboard with this.props.navigation.navigate when the store state changes.
I can do this hackily:
render() {
const {account} = this.props
const {token} = account
if (token) {
this.props.navigation.navigate('Dashboard')
}
}
I can also use callbacks:
sendRegistration = () => {
const {email, password} = this.getFormFields()
this.props.registerStart({email, password, success: this.onRegisterSuccess, failure: this.onRegisterFailure}) //using mapDispatchToProps
}
Passing the callback through the redux path seems redundant since I already have the changed state thanks to linking the account store to my Registration component props.
I am toying with the idea of a top-level renderer that detects a change in a userScreen store then swaps out the appropriate component to render.
Is there a simpler, or better way?
Yes there is a better way. If you want to navigate in an async fashion the best place to do it is directly in the thunk, sagas, etc after the async action is successful. You can do this by creating a navigation Service that uses the ref from your top level navigator to navigate.
In app.js:
import { createStackNavigator, createAppContainer } from 'react-navigation';
import NavigationService from './NavigationService';
const TopLevelNavigator = createStackNavigator({
/* ... */
});
const AppContainer = createAppContainer(TopLevelNavigator);
export default class App extends React.Component {
// ...
render() {
return (
<AppContainer
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
);
}
}
This sets the ref of the navigator. Then in your NavigationService file:
// NavigationService.js
import { NavigationActions } from 'react-navigation';
let _navigator;
function setTopLevelNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName, params) {
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
Now you have access to the navigator and can navigate from redux directly. You can use it like this:
// any js module
import NavigationService from 'path-to-NavigationService.js';
// ...
NavigationService.navigate(''Dashboard' });
Here is the documentation explaining more:
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html

How to dispatch actions from Child components three level down?

I am currently facing this issue designing a React application and I don't seem to be able to find an answer to it.
So my application has following heirarchy of Components in React Router
App
-> DynamicContainer
-> -> LoginComponent
Now, LoginComponents has form elements to take username and password.
I have userActionCreators where the login is handled, and it dispatches login successful when finished, but I don't seem to be able find the right way to connect my LoginComponent to dispatch actions or call actionCreators.
How do I do it? Any suggestion would be appreciated.
One option is to bind your single-purpose forms to their actions with connect. Since <LoginComponent /> is typically always doing the exact same thing, you can use it like this:
import React from 'react';
import * as userActionCreators from '../actions/users';
import { connect } from 'react-redux';
export class LoginComponent extends React.Component {
static propTypes = {
login: React.PropTypes.func.isRequired
}
render() {
const { login } = this.props;
const { username, password } = this.state;
return (
<form onSubmit={ () => login(username, password) }>
...
</form>
);
}
}
export default connect(null, userActionCreators)(LoginComponent);
connect automatically binds the action creator and separately provides dispatch to props, so if you want to be more explicit, the above example is the same as
import React from 'react';
import { login } from '../actions/users';
import { connect } from 'react-redux';
export class LoginComponent extends React.Component {
static propTypes = {
dispatch: React.PropTypes.func.isRequired
}
render() {
const { login, dispatch } = this.props;
const { username, password } = this.state;
return (
<form onSubmit={ () => dispatch(login(username, password)) }>
...
</form>
);
}
}
export default connect()(LoginComponent);
And for reference, userActionCreators:
const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
const LOGIN_FAILED = 'LOGIN_FAILED';
export function login(username, password) {
if (username === 'realUser' && password === 'secretPassword') {
return { type: LOGIN_SUCCESS, payload: { name: 'Joe', username: 'realUser' } };
} else {
return { type: LOGIN_FAILED, error: 'Invalid username or password };
}
}
if I understood you correctly, if you read Example: Todo List | Redux you'll find the example that you might be looking for.
There's the App component, connect()ed to Redux, and then there're the other components: AddTodo, TodoList, Todo and Footer.
App calls TodoList that calls Todo where user can click something. This click will surf back callback after callback, from Todo to TodoList to App as detailed below:
App calls TodoList with
<TodoList todos={visibleTodos} onTodoClick={ index => dispatch(completeTodo(index)) } />
TodoList calls Todo with
<Todo {...todo} key={index} onClick={ () => this.props.onTodoClick(index) } />
Todo component has a <li> with onClick={this.props.onClick} property.
So, backwards, when someones clicks inside the Todo compoment, that will call this.props.onClick which will call this.props.onTodoClick(index) from TodoList (notice the optional added parameter index), then, at last, this will invoke the function dispatch(completeTodo(index)) from App.
Two options:
Pass a bound actionCreator from your Container (which is connected to Redux) down to a child component (which is not connected to Redux) via the props object.
Consider adopting React Component Context in your project.

Resources