Redux/Firebase - Component not updating after uid is received in state - reactjs

I'm using Firebase's authentication API for my Redux app. I have an app component that is supposed to be informed when a user's UID authentication is received and toggle which header is shown to the user (either logged in header or logged out header). However, in instances where the user is already logged in and revisiting the main route, I can't seem to get my main app to re-render when the UID is stored to the app state.
An outline of the flow is such:
In routes/index.js: Firebase's onAuthStateChanged observer
identifies if the user is already logged in or not when the main
route loads. If there's a user, dispatch actions to copy their UID
from Firebase to our state and send them to the "connect" page.
In actions.jsx: The startLoginForAuthorizedUser action creator
dispatches an action to update the auth reducer with the new UID &
reroute the user to "connect".
In reducers.jsx: The "auth" state is updated to include the user's
UID to allow components to toggle elements depending on
authentication status.
In App.jsx: For whatever reason, mapStateToProps is not receiving an
updated state, even though the user is authenticated and Redux dev
tools shows the state as updated with the new UID.
The end result is that authenticated users see the "connect" page as expected, but still see a logged-out header. Here's the code:
App.jsx
import React from 'react';
import { connect } from 'react-redux';
import * as actions from 'actions';
import HeaderLoggedOut from './HeaderLoggedOut';
import ModalOverlay from './ModalOverlay';
import MenuWrapper from './MenuWrapper';
import Header from './Header';
import Tabs from './Tabs';
export const App = React.createClass({
render(){
const { uid } = this.props;
console.log("App.jsx: uid:", uid);
if(!uid){
return(
<div>
<HeaderLoggedOut />
<div className="tonal-main">
<div className="tonal-content">
{ this.props.children }
</div>
</div>
<ModalOverlay />
</div>
);
}
return(
<MenuWrapper>
<Header />
<div className="tonal-content">
{ this.props.children }
</div>
<Tabs />
</MenuWrapper>
);
}
});
const mapStateToProps = (state) => {
// state is not updated
return {
uid: state.auth.uid
};
};
export default connect(mapStateToProps)(App);
router/index.js
import App from 'App.jsx';
import Connect from 'connect/Connect.jsx';
import firebase from 'app/firebase';
const store = require('store').configure();
firebase.auth().onAuthStateChanged((user) => {
if(user.emailVerified && user.uid){
store.dispatch(actions.startLoginForAuthorizedUser(user.uid));
} else {
browserHistory.push('/');
}
});
const requireLogin = (nextState, replace, next) => {
const currentUser = firebase.auth().currentUser;
if(!currentUser){
replace('/');
}
next();
};
const redirectIfLoggedIn = (nextState, replace, next) => {
const currentUser = firebase.auth.currentUser;
if(currentUser){
replace('/connect');
}
next();
};
export default (
<Router history={ browserHistory }>
<Route path="/" component={ App }>
<IndexRoute component={ Landing } onEnter={ redirectIfLoggedIn } />
<Route path="connect" component = { Connect } onEnter = { requireLogin } />
</Route>
</Router>
);
Actions.jsx
// ...actions, imports, etc...
export var startLoginForAuthorizedUser = (uid) => {
return (dispatch) => {
dispatch(login(uid));
dispatch(pushToRoute('/connect'));
};
};
export var login = (uid) => {
console.log('actions: logging in user with uid ', uid);
return{
type: 'LOGIN',
uid
};
};
export var pushToRoute = (route) => {
browserHistory.push(route);
};
Reducer.jsx
const authInitialState = {
uid: ""
};
export const authReducer = (state = authInitialState, action) => {
switch(action.type){
case 'LOGIN':
return {
...state,
uid: action.uid
};
case 'LOGOUT':
return {
...state,
uid: ""
};
default:
return state;
}
};
Any help from you experts would be greatly appreciated. This is driving me nuts.

I figured it out after an embarrassing amount of trial and error. For anyone else in the future who wants to use both react-router/react-router-dom and firebase's onAuthStateChanged() observer, just know you have to keep the observer code at your topmost component (where you render your provider in ReactDOM.render()).
If you keep it anywhere else, it doesn't appear to work properly and I frankly have no idea why. If someone more familiar with firebase wanted to explain what was happening there and why it would work in the top-level component but not in the routes index file, I'm sure it would help someone down the road.

Related

How to Redirect automatically to a page/path on login - MSAL React SPA

I am working on a single page application (SPA) app that grants access to specific paths in the application, based on roles setup in Azure AD for the user logging in. As per this https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/5-AccessControl/1-call-api-roles
This is my 'authConfig.js' file - you can see the redirectUri
const clientId = window.REACT_APP_CLIENTID
export const msalConfig = {
auth: {
clientId: clientId,
authority: window.REACT_APP_AUTHORITY,
redirectUri: 'http://localhost:3000/todolist/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.origin
postLogoutRedirectUri: "/", // Indicates the page to navigate after logout.
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
},
cache: {
cacheLocation: "sessionStorage", // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};
/**
* Add here the endpoints and scopes when obtaining an access token for protected web APIs. For more information, see:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
*/
export const protectedResources = {
apiTodoList: {
todoListEndpoint: window.REACT_APP_APIENDPOINT+"/api/v2/support/list",
scopes: [window.REACT_APP_APIENDPOINT+"/access_as_user"],
},
}
/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: [...protectedResources.apiTodoList.scopes]
};
export const appRoles = {
TaskUser: "TaskUser",
TaskAdmin: "TaskAdmin",
TrialAdmin: "Trial.Admin",
GlobalAdmin: "Global.Admin"
}
Here is the App.jsx file (I believe there needs to be some change made here). You can see 'RouteGuard' that renders the Component {TodoList}, when the path 'todolist' is accessed.
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { MsalProvider } from "#azure/msal-react";
import { RouteGuard } from './components/RouteGuard';
import { PageLayout } from "./components/PageLayout";
import { TodoList } from "./pages/TodoList";
import { appRoles } from "./authConfig";
import "./styles/App.css";
const Pages = () => {
return (
<Switch>
<RouteGuard
exact
path='/todolist/'
roles={[appRoles.TaskUser, appRoles.TaskAdmin, appRoles.TrialAdmin, appRoles.GlobalAdmin]}
Component={TodoList}
/>
</Switch>
)
}
/**
* msal-react is built on the React context API and all parts of your app that require authentication must be
* wrapped in the MsalProvider component. You will first need to initialize an instance of PublicClientApplication
* then pass this to MsalProvider as a prop. All components underneath MsalProvider will have access to the
* PublicClientApplication instance via context as well as all hooks and components provided by msal-react. For more, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/getting-started.md
*/
const App = ({ instance }) => {
return (
<Router>
<MsalProvider instance={instance}>
<PageLayout>
<Pages instance={instance} />
</PageLayout>
</MsalProvider>
</Router>
);
}
export default App;
So as far as my understanding goes, the path 'todolist' is accessed with the listed role, and component is rendered
When logged in, The navigation bar at the top renders with login request, after authentication (). It has the button rendered, with a click function that 'href's to the path '/todolist'.
import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "#azure/msal-react";
import { Nav, Navbar, Button, Dropdown, DropdownButton} from "react-bootstrap";
import React, { useState, useEffect } from "react";
import { loginRequest } from "../authConfig";
import { InteractionStatus, InteractionType } from "#azure/msal-browser";
import "../styles/App.css";
import logo from "../public/images/logo.jpg";
export const NavigationBar = (props) => {
const { instance } = useMsal();
const { inProgress } = useMsal();
const [isAuthorized, setIsAuthorized] = useState(false);
//The below function is needed incase you want to login using Popup and not redirect
const handleLogin = () => {
instance.loginPopup(loginRequest)
.catch((error) => console.log(error))
}
/**
* Most applications will need to conditionally render certain components based on whether a user is signed in or not.
* msal-react provides 2 easy ways to do this. AuthenticatedTemplate and UnauthenticatedTemplate components will
* only render their children if a user is authenticated or unauthenticated, respectively.
*/
return (
<>
<Navbar className="color-custom" variant="dark">
<a className="navbar-brand" href="/"><img src={logo} className="navbarLogo" alt="TODDOLIST1"/></a>
<AuthenticatedTemplate>
<Nav.Link as={Button} id="signupbutton" variant="dark" className="signupNav" href="/todolist"><strong>List</strong></Nav.Link>
<Button variant="warning" className="ml-auto" drop="left" title="Sign Out" onClick={() => instance.logoutRedirect({ postLogoutRedirectUri: "/" })}><strong>Sign Out</strong></Button>
</AuthenticatedTemplate>
<UnauthenticatedTemplate>
<Button variant="dark" className="ml-auto" drop="left" title="Sign In" onClick={() => instance.loginRedirect(loginRequest)}>Sign In</Button>
</UnauthenticatedTemplate>
</Navbar>
</>
);
};
Here is the RouteGuard.jsx component that renders based on roles/authorization.
import React, { useState, useEffect } from "react";
import { Route } from "react-router-dom";
import { useMsal } from "#azure/msal-react";
export const RouteGuard = ({ Component, ...props }) => {
const { instance } = useMsal();
const [isAuthorized, setIsAuthorized] = useState(false);
const onLoad = async () => {
const currentAccount = instance.getActiveAccount();
if (currentAccount && currentAccount.idTokenClaims['roles']) {
let intersection = props.roles
.filter(role => currentAccount.idTokenClaims['roles'].includes(role));
if (intersection.length > 0) {
setIsAuthorized(true);
}
}
}
useEffect(() => {
onLoad();
}, [instance]);
return (
<>
{
isAuthorized
?
<Route {...props} render={routeProps => <Component {...routeProps} />} />
:
<div className="data-area-div">
<h3>You are unauthorized to view this content.</h3>
</div>
}
</>
);
};
I want the application to directly go to the '/todolist' and render the components within. My redirect uri, does not seem to work. When i login with the required role, it always renders 'You are unauthorized to view this content' as per the RouteGuard file. The URI is /signuplist/ but still the children props are not rendered. ONLY WHEN I CLICK the button 'Todolist' (as per NavigationBar.jsx), does it go and render the child props properly. Redirection does not work as expected. I want it to directly go to /todolist and render the page, child components
Any suggestions ?
In case my last comment worked out for you, let me make it into an official answer so it can be recorded.
Basically,
define: const { instance, accounts, inProgress } = useMsal();
Then try to redirect when inProgress !== 'login'.
Can you try removing the async from onload method signature ?
Did you registered 'http://localhost:3000/todolist/' as a valid redirect uri on Azure portal. You can have multiple redirect uri as per the role, but all the uri must be registred on Azure portal under you app.
https://learn.microsoft.com/en-ca/azure/active-directory/develop/quickstart-register-app#register-an-application
A redirect URI is the location where the Microsoft identity platform redirects a user's client and sends security tokens after authentication.Is

Redux Store not populated before Component Render

I am working on the authentification procedure for an app I'm developing.
Currently, the user logins in through Steam. Once the login is validated the server redirects the user to the app index, /, and issues them a pair of JWTs as GET variables. The app then stores these in a Redux store before rewriting the URL to hide the JWT tokens for security purposes.
The app then decodes the tokens to obtain info about the user, such as their username and avatar address. This should be rendered in the app's SiteWrapper component, however, this is where my problem occurs.
What seems to be happening is SiteWrapper component loads before the App component finishes saving the tokens and thus throws errors as variables are not defined. Most of the fixes that seem relevant are for API requests, however, in this case, that is not the case. I already have the data in the URL. I'm not sure if the same applies.
Any suggestions on how to fix this? Any other best practice advice would be appreciated. I'm new to both React and Redux.
Error
Index
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from './redux/store';
// console debug setup
window.store = store;
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
serviceWorker.unregister();
App
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { connect } from 'react-redux';
import "tabler-react/dist/Tabler.css";
import history from './utils/history';
import {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
} from "./redux/app";
import {
HomePage
} from './pages';
class App extends Component {
componentDidMount() {
//Get tokens from URL when app loads and then hide them from url.
const urlParams = new URLSearchParams(window.location.search);
if(urlParams.has('access_token') && urlParams.has('refresh_token')){
this.props.storeRefreshJWTToken(urlParams.get('refresh_token'));
this.props.storeAccessJWTToken(urlParams.get('access_token'));
//Load user info from obtained tokens.
this.props.loadUserFromJWTRefreshToken();
}
history.push('/');
}
render() {
return (
<React.StrictMode>
<Router basename={process.env.PUBLIC_URL} history={history}>
<Switch>
<Route exact path="/" component={HomePage}/>
</Switch>
</Router>
</React.StrictMode>
);
}
}
const mapStateToProps = (state) => ({});
const mapDispatchToProps = {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
SiteWrapper
import * as React from "react";
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import {
Site,
Nav,
Grid,
List,
Button,
RouterContextProvider,
} from "tabler-react";
class SiteWrapper extends React.Component {
componentDidMount() {
console.log(this.props);
this.accountDropdownProps = {
avatarURL: this.props.user.avatar,
name: this.props.user.display_name,
description: "temp",
options: [
{icon: "user", value: "Profile"},
{icon: "settings", value: "Settings"},
{isDivider: true},
{icon: "log-out", value: "Sign out"},
],
};
}
render(){
return (
<Site.Wrapper
headerProps={{
href: "/",
alt: "Tabler React",
imageURL: "./demo/brand/tabler.svg",
navItems: (
<Nav.Item type="div" className="d-none d-md-flex">
<Button
href="https://github.com/tabler/tabler-react"
target="_blank"
outline
size="sm"
RootComponent="a"
color="primary"
>
Source code
</Button>
</Nav.Item>
),
accountDropdown: this.accountDropdownProps,
}}
navProps={{ itemsObjects: this.props.NavBarLinks }}
routerContextComponentType={withRouter(RouterContextProvider)}
footerProps={{
copyright: (
<React.Fragment>
Copyright © 2018
Thomas Smyth.
All rights reserved.
</React.Fragment>
),
nav: (
<React.Fragment>
<Grid.Col auto={true}>
<List className="list-inline list-inline-dots mb-0">
<List.Item className="list-inline-item">
Developers
</List.Item>
<List.Item className="list-inline-item">
FAQ
</List.Item>
</List>
</Grid.Col>
</React.Fragment>
),
}}
>
{this.props.children}
</Site.Wrapper>
);
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
user: state.App.user
};
};
export default connect(mapStateToProps)(SiteWrapper);
Reducer
import initialState from './initialState';
import jwt_decode from 'jwt-decode';
//JWT Auth
const STORE_JWT_REFRESH_TOKEN = "STORE_JWT_REFRESH_TOKEN";
export const storeRefreshJWTToken = (token) => ({type: STORE_JWT_REFRESH_TOKEN, refresh_token: token});
const STORE_JWT_ACCESS_TOKEN = "STORE_JWT_ACCESS_TOKEN";
export const storeAccessJWTToken = (token) => ({type: STORE_JWT_ACCESS_TOKEN, access_token: token});
// User
const LOAD_USER_FROM_JWT_REFRESH_TOKEN = "DEC0DE_JWT_REFRESH_TOKEN";
export const loadUserFromJWTRefreshToken = () => ({type: LOAD_USER_FROM_JWT_REFRESH_TOKEN});
export default function reducer(state = initialState.app, action) {
switch (action.type) {
case STORE_JWT_REFRESH_TOKEN:
return {
...state,
jwtAuth: {
...state.jwtAuth,
refresh_token: action.refresh_token
}
};
case STORE_JWT_ACCESS_TOKEN:
return {
...state,
JWTAuth: {
...state.jwtAuth,
access_token: action.access_token
}
};
case LOAD_USER_FROM_JWT_REFRESH_TOKEN:
const user = jwt_decode(state.jwtAuth.refresh_token);
return {
...state,
user: user
};
default:
return state;
}
}
Store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk'
import App from './app';
const combinedReducers = combineReducers({
App
});
const store = createStore(combinedReducers, applyMiddleware(ReduxThunk));
export default store;
componentDidMount() is going to run after first render. So on first render the this.props.user inside will be null at that point.
You could:
move the async call to componentWillMount() (not recommended)
put a guard in the SiteWrapper() so it can handle null case
not render Home from the App until the async call has finished
in componentDidMount() you are expecting user to be set.
You can:
Set user in initial state with some "isAuth" flag that you can easily check
As componentWillMount() is deprecated you can use componentDidUpdate() to check if user state is changed and do some actions.
Use asyns functions when sawing YWT
You could of course check for null and don't try to show the user's details in your SiteWrapper's componentDidMount()-method. But why? Do you have an alternative route to go if your user couldn't be found and is null? I guess no. So you basically have two options:
The ideal solution is to implement async actions and show an activity
indicator (e.g. spinner) until the jwt-token is loaded. Afterwards
you can extract your user information and fully render your
component as soon as the fetch is succesfully completed.
If you can't use async action, for whatever reason, I would suggest
the "avoid-null" approach. Put a default user in your initial
state and it should be done. If you update the user prop, the
component will rerender anyways (if connected properly).
I have solved my issue. It seems this was one of those rare cases where trying to keep things simple and develop was bad.
Now, when my application loads I either show the user information or a login button.
Once the key is loaded from the URL the component rerenders to show the user information.
This does increase the number of renders, however, it is probably the best way of doing it.

Redirect router on after login

I am trying to figure out how to redirect my react app to proper page after it has been authenticated through the login.
Here is my App.js file with the routed (without imports):
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<Switch>
<PrivateRoute path="/test" component={Test} />
<Route path="/login" component={Login} />
<PrivateRoute path="/" render={() => <h1>Welcome</h1>} />
</Switch>
</Router>
</Provider>
), document.getElementById('root'));
I am using PrivateRoute component to make sure private routes get authenticated:
class PrivateRoute extends Route {
render() {
const { component: Component, isAuthenticated } = this.props;
let propsCopy = Object.assign({}, this.props);
delete propsCopy.component;
delete propsCopy.isAuthenticated;
return (
isAuthenticated
? <Component {...propsCopy} />
: <Redirect to={{
pathname: LOGIN_PATH,
state: { from: this.props.location }
}} />
);
}
}
/**
* Maps properties from Redux store to this component.
* #param {Object} state Redux object
* #return {Object} mapper properties
*/
function mapStateToProps(state) {
// pull out auth element from Redux store state
const { auth } = state;
// extract authenticated element from auth object
const { isAuthenticated } = auth;
return {
isAuthenticated
}
}
export default connect(mapStateToProps)(PrivateRoute);
My Login component that get redirected to (simplified for the sake of the example:
class LoginForm extends Component {
constructor() {
super();
this.state = {
email: '',
password: '',
}
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(event) {
//event.preventDefault();
const validation = this.validator.validate(this.state);
this.setState({ validation });
this.submitted = true;
if (validation.isValid) {
// submit form here
this.props.loginUser({
email: this.state.email,
password: this.state.password
});
}
}
render() {
return (
// My login FROM code here
)
}
}
function mapStateToProps(state) {
return {
isFetching: state.auth.isFetching,
loginError: state.auth.loginError,
isAuthenticated: state.auth.isAuthenticated
};
}
function mapDispatchToProps(dispatch, props, state) {
return {
loginUser: (credentials) => {
dispatch(loginUser(credentials));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
Right now it works where it redirects to the LoginForm component if isAuthenticated is false. I can submit the login form to my login service and receive a success response and set isAuthenticated.
My question is how to I redirect to the original route? Where is redirection normally done? I'm assuming it's never done in the reducer so it would have to be done in the LoginForm component right?
I know there are a lot of resources discuss this whole login flow but I can't find one that deals with this issue(which surprised me). Everyone redirects to a specific page ('/', '/home' etc) but how do i capture and redirect to the original route.
The PrivateRoute component is storing the previous route in from when a redirect occurs due to isAuthenticated being false. This can be used in LoginForm to redirect the user when isAuthenticated is true. Just extract from from this.props.location.state and use that in combination with Redirect component from react-router-dom. If you log this.props.location.state.from you will see the property pathname containing the string route path that the user attempted to get to in an unauthenticated state, which can be used to redirect them once authentication is successful.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
}
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(event) {
event.preventDefault();
const validation = this.validator.validate(this.state);
this.setState({ validation });
this.submitted = true;
if (validation.isValid) {
// submit form here
this.props.loginUser({
email: this.state.email,
password: this.state.password
});
}
}
render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { isAuthenticated } = this.props;
if (isAuthenticated) {
return <Redirect to={from} />;
}
return (
{* login from code *}
);
}
}
function mapStateToProps(state) {
return {
isFetching: state.auth.isFetching,
loginError: state.auth.loginError,
isAuthenticated: state.auth.isAuthenticated
};
}
function mapDispatchToProps(dispatch, props, state) {
return {
loginUser: (credentials) => {
dispatch(loginUser(credentials));
},
};
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
You may need to update your PrivateRoute component is well to ensure it returns a Route, this would be in line with react-router-dom example:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props =>
rest.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: LOGIN_PATH,
state: { from: props.location }
}}
/>
)
}
/>
);
const mapStateToProps = ({ auth: { isAuthenticated } }) => ({ isAuthenticated });
export default withRouter(connect(mapStateToProps)(PrivateRoute));
I've created a simplified StackBlitz demonstrating the functionality in action.
Hopefully that helps!
#Alexander and #bos570 first of all I really appreciate the way you've done this. There are better approaches to this but here is an implementation to build it in controlled way:
In your index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './styles.scss'; // it depends what styles you're using css,scss,jss
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
In your App.js:
class App extends Component {
state = {
isAuthenticated: false,
};
componentDidMount(){
//call authentication API here
// setState isAuthentication to true upon authentication success
}
render(){
return(
<Provider store={store}>
<BrowserRouter>{isAuthenticated ? <Routes /> : <Login />}</BrowserRouter>
</Provider>
)}
Your Routes.js will have all routes in a Switch and It will keep showing loading screen unless API response gives success. and you re-route to a basic Login page.
Even If you've done code splitting It would not download any bundles from network except main.bundle.js upon authentication failure.
I see people when people use Redux like jack of all trades in programming. I know you future implementation for authentication will give you an refreshToken that's what Authentication API does for us. You can store it in localStorage and You need to do this if someone refresh the browser at any instant you can't keep it in redux as we know redux will lose state upon browser refresh so here localStorage comes into the play. Use it wisely I would really like to have conversion with both of you how can we make it more better.
I hope this would be helpful for you. and Maybe redux will be used side by side for Authentication. I have had a great time while implementing auth in an app.
Cheers
EDITED keep it as it you've done. But we need to know at which point, you need redux, localStorage, Context API

Getting a value from a particular state before component renders using react/redux?

I am using react/redux with a nodejs(express)/mongodb back-end incase that helps.
What I want to happen here is that if a user tries to go to edit a post that does not belong to them I want them to be re routed immediately and never see that page.
For example. User "A" goes to route localhost:8080/posts/post_id/edit, but that post belongs to user "B". I want User A to immediately get re routed back to that post or localhost:8080/posts/post_id.
In my code I can get the user through a action called getUser() which sends an axios.get request to the back-end to get the current user who is logged in. I am using JWT token. Not sure if this is information needed or not.
Here is the code to show you what I am trying to do.
import React , { Component } from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../../actions/posts_actions';
import * as actionsIndex from '../../actions/index';
import { reduxForm, Field } from 'redux-form';
import {connect} from 'react-redux';
import {Link} from 'react-router-dom';
class EditPost extends Component {
componentWillMount() {
if(this.props.auth) {
console.log(this.props.auth); // -> returns true
this.props.getUser(); // -> this fires off
}
}
componentDidMount() {
const {id} = this.props.match.params;
this.props.getOnePost(id);
if(this.props.auth){
if(this.props.user._id !== this.props.post.author.id){
this.props.history.push(`/posts/${id}`);
}
}
}
renderField(field) {
const { meta: {touched, error} } = field;
const className = `form-group ${touched && error ? 'has-danger' : ''}`;
return (
<div className={className}>
<label><strong>{field.label}:</strong></label>
<input
className="form-control"
type={field.type}
{...field.input}
/>
<div className="text-help">
{ touched ? error : ''}
</div>
</div>
)
}
onSubmit(values) {
const {id} = this.props.match.params;
this.props.updatePost(values, id, () => {
this.props.history.push(`/posts/${id}`);
});
}
render() {
const {handleSubmit} = this.props;
const {id} = this.props.match.params;
console.log(this.props.user); // -> shows me the user after three nulls
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<Field
label="Title"
name="title"
type="text"
component={this.renderField}
/>
<Field
label="Content"
name="content"
type="text"
component={this.renderField}
/>
<button type="submit" className="btn btn-success">Submit</button>
<Link to={`/posts/${id}`} className="btn btn-danger">Cancel</Link>
</form>
);
}
}
function validate(values) {
const errors = {};
if(!values.title) {
errors.title = "Enter a title!";
}
if(!values.content) {
errors.content = "Enter some content please!";
}
return errors;
}
function mapStateToProps({ posts, auth, user }, ownProps) {
return {
initialValues: posts[ownProps.match.params.id],
post: posts[ownProps.match.params.id],
auth: auth.authenticated,
user: user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({...actions, ...actionsIndex}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({
validate,
form: 'editform'
})(EditPost));
Here are the console.log statements:
Here is an edit of the index.js page , is there some way I could update the user state here?:
"use strict"
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import { applyMiddleware, createStore } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers/index';
import App from './components/app';
import '../style/style.css';
import Home from './components/pages/home';
import Header from './components/header';
import Footer from './components/footer';
import RequireAuth from './components/auth/require_auth';
import RequireUnAuth from './components/auth/require_unauth';
import Signin from './components/auth/signin';
import Signup from './components/auth/signup';
import Signout from './components/auth/signout';
import Posts from './components/pages/posts';
import {AUTH_USER} from './actions/types';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);
const token = localStorage.getItem('token');
if(token) {
store.dispatch({ type: AUTH_USER });
}
const Routes = (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
ReactDOM.render(Routes, document.querySelector('.container'));
If this.props.getUser() is an async (thunk-ed) action creator, you're not guaranteed to have the result of that available by the time you reach componentDidMount
Add a null check for this.props.user in componentDidMount before you attempt to access fields on it.
Something you could do is move
if(this.props.user._id !== this.props.post.author.id){
this.props.history.push(`/posts/${id}`);
}
into componentWillReceiveProps
componentWillReceiveProps ({ user: nextUser }) {
const { history, match, user: currentUser, post } = this.props
const { id } = match.params
/* if you didn't previously have currentUser (i.e. after first load) - !currentUser
* or the user has changed - nextUser !== currentUser*/
if (!currentUser || nextUser !== currentUser) { // probably shouldn't use === here
/* the component might update before `this.props.getUser()` "returns", so make sure this is the
* update we are looking for by ensuring that we have nextUser - nextUser &&
* then get the id (nextUser._id), and run the check (!== post.author.id)*/
if (nextUser && (nextUser._id !== post.author.id)) {
history.push(`/posts/${id}`)
}
}
}
Here's a little light Friday reading on componentWillReceiveProps - might clear some things up.
A couple things regarding your edit:
An HoC might be a good idea if you have a lot of components that rely on user
AuthComponent HoC concept
class AuthComponent extends React.Component {
componentDidMount() {
if (!this.props.user) {
this.props.getUser()
}
}
render() {
// you could inject `user` into children here, but I would suggest that you
// instead store the result of `getUser` into a store like `redux` or `flux` and fetch from that in child components
return this.props.children
}
}
Obviously you'll need to use connect and grab user out of state for this component. You would use it like
class SomeComponent extends React.Component {
render () {
return (
<AuthComponent>
<WrappedComponent/> // <- this is what's rendered
</AuthComponent>
)
}
}
you're using react-router, though, so a better solution would be to incorporate it into your routing
<Route path="" component={AuthComponent}>
<Route path="some-path" component={WrappedComponent}/>
</Route>
like I said in the comment, cache and return user from your store... although it looks like you're already doing this
don't assume that you'll always have access to user - make sure you're null checking it
The code that you are using in your componentWillMount does not belong here.
this.props.getUser();
You better create an action redux-thunk creator which deals with promise + async actions and only then return the result through the state/dispatch mechanism to be later used in componentDidMount lifecycle hook.
Redux thunk example to call API, please take a look at redux-thunk docs:
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument(api))
)
// later
function fetchUser(id) {
return (dispatch, getState, api) => {
// you can use api here
}
}
To pass multiple things, just wrap them in a single object and use destructuring:
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({ api, whatever }))
)
// later
function fetchUser(id) {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here here
}
}

React/Redux - dispatch action on app load/init

I have token authentication from a server, so when my Redux app is loaded initially I need make a request to this server to check whether user is authenticated or not, and if yes I should get token.
I have found that using Redux core INIT actions is not recommended, so how can I dispatch an action, before app is rendered?
You can dispatch an action in Root componentDidMount method and in render method you can verify auth status.
Something like this:
class App extends Component {
componentDidMount() {
this.props.getAuth()
}
render() {
return this.props.isReady
? <div> ready </div>
: <div>not ready</div>
}
}
const mapStateToProps = (state) => ({
isReady: state.isReady,
})
const mapDispatchToProps = {
getAuth,
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
All of the answers here seem to be variations on creating a root component and firing it in the componentDidMount. One of the things I enjoy most about redux is that it decouples data fetching from component lifecycles. I see no reason why it should be any different in this case.
If you are importing your store into the root index.js file, you can just dispatch your action creator(let's call it initScript()) in that file and it will fire before anything gets loaded.
For example:
//index.js
store.dispatch(initScript());
ReactDOM.render(
<Provider store={store}>
<Routes />
</Provider>,
document.getElementById('root')
);
I've not been happy with any solutions that have been put forward for this, and then it occurred to me that I was thinking about classes needing to be rendered. What about if I just created a class for startup and then push things into the componentDidMount method and just have the render display a loading screen?
<Provider store={store}>
<Startup>
<Router>
<Switch>
<Route exact path='/' component={Homepage} />
</Switch>
</Router>
</Startup>
</Provider>
And then have something like this:
class Startup extends Component {
static propTypes = {
connection: PropTypes.object
}
componentDidMount() {
this.props.actions.initialiseConnection();
}
render() {
return this.props.connection
? this.props.children
: (<p>Loading...</p>);
}
}
function mapStateToProps(state) {
return {
connection: state.connection
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Startup);
Then write some redux actions to async initialise your app. Works a treat.
If you are using React Hooks, one single-line solution is
useEffect(() => store.dispatch(handleAppInit()), []);
The empty array ensures it is called only once, on the first render.
Full example:
import React, { useEffect } from 'react';
import { Provider } from 'react-redux';
import AppInitActions from './store/actions/appInit';
import store from './store';
export default function App() {
useEffect(() => store.dispatch(AppInitActions.handleAppInit()), []);
return (
<Provider store={store}>
<div>
Hello World
</div>
</Provider>
);
}
Update 2020:
Alongside with other solutions, I am using Redux middleware to check each request for failed login attempts:
export default () => next => action => {
const result = next(action);
const { type, payload } = result;
if (type.endsWith('Failure')) {
if (payload.status === 401) {
removeToken();
window.location.replace('/login');
}
}
return result;
};
Update 2018: This answer is for React Router 3
I solved this problem using react-router onEnter props. This is how code looks like:
// this function is called only once, before application initially starts to render react-route and any of its related DOM elements
// it can be used to add init config settings to the application
function onAppInit(dispatch) {
return (nextState, replace, callback) => {
dispatch(performTokenRequest())
.then(() => {
// callback is like a "next" function, app initialization is stopped until it is called.
callback();
});
};
}
const App = () => (
<Provider store={store}>
<IntlProvider locale={language} messages={messages}>
<div>
<Router history={history}>
<Route path="/" component={MainLayout} onEnter={onAppInit(store.dispatch)}>
<IndexRoute component={HomePage} />
<Route path="about" component={AboutPage} />
</Route>
</Router>
</div>
</IntlProvider>
</Provider>
);
With the redux-saga middleware you can do it nicely.
Just define a saga which is not watching for dispatched action (e.g. with take or takeLatest) before being triggered. When forked from the root saga like that it will run exactly once at startup of the app.
The following is an incomplete example which requires a bit of knowledge about the redux-saga package but illustrates the point:
sagas/launchSaga.js
import { call, put } from 'redux-saga/effects';
import { launchStart, launchComplete } from '../actions/launch';
import { authenticationSuccess } from '../actions/authentication';
import { getAuthData } from '../utils/authentication';
// ... imports of other actions/functions etc..
/**
* Place for initial configurations to run once when the app starts.
*/
const launchSaga = function* launchSaga() {
yield put(launchStart());
// Your authentication handling can go here.
const authData = yield call(getAuthData, { params: ... });
// ... some more authentication logic
yield put(authenticationSuccess(authData)); // dispatch an action to notify the redux store of your authentication result
yield put(launchComplete());
};
export default [launchSaga];
The code above dispatches a launchStart and launchComplete redux action which you should create. It is a good practice to create such actions as they come in handy to notify the state to do other stuff whenever the launch started or completed.
Your root saga should then fork this launchSaga saga:
sagas/index.js
import { fork, all } from 'redux-saga/effects';
import launchSaga from './launchSaga';
// ... other saga imports
// Single entry point to start all sagas at once
const root = function* rootSaga() {
yield all([
fork( ... )
// ... other sagas
fork(launchSaga)
]);
};
export default root;
Please read the really good documentation of redux-saga for more information about it.
Here's an answer using the latest in React (16.8), Hooks:
import { appPreInit } from '../store/actions';
// app preInit is an action: const appPreInit = () => ({ type: APP_PRE_INIT })
import { useDispatch } from 'react-redux';
export default App() {
const dispatch = useDispatch();
// only change the dispatch effect when dispatch has changed, which should be never
useEffect(() => dispatch(appPreInit()), [ dispatch ]);
return (<div>---your app here---</div>);
}
I was using redux-thunk to fetch Accounts under a user from an API end-point on app init, and it was async so data was coming in after my app rendered and most of the solutions above did not do wonders for me and some are depreciated. So I looked to componentDidUpdate(). So basically on APP init I had to have accounts lists from API, and my redux store accounts would be null or []. Resorted to this after.
class SwitchAccount extends Component {
constructor(props) {
super(props);
this.Format_Account_List = this.Format_Account_List.bind(this); //function to format list for html form drop down
//Local state
this.state = {
formattedUserAccounts : [], //Accounts list with html formatting for drop down
selectedUserAccount: [] //selected account by user
}
}
//Check if accounts has been updated by redux thunk and update state
componentDidUpdate(prevProps) {
if (prevProps.accounts !== this.props.accounts) {
this.Format_Account_List(this.props.accounts);
}
}
//take the JSON data and work with it :-)
Format_Account_List(json_data){
let a_users_list = []; //create user array
for(let i = 0; i < json_data.length; i++) {
let data = JSON.parse(json_data[i]);
let s_username = <option key={i} value={data.s_username}>{data.s_username}</option>;
a_users_list.push(s_username); //object
}
this.setState({formattedUserAccounts: a_users_list}); //state for drop down list (html formatted)
}
changeAccount() {
//do some account change checks here
}
render() {
return (
<Form >
<Form.Group >
<Form.Control onChange={e => this.setState( {selectedUserAccount : e.target.value})} as="select">
{this.state.formattedUserAccounts}
</Form.Control>
</Form.Group>
<Button variant="info" size="lg" onClick={this.changeAccount} block>Select</Button>
</Form>
);
}
}
const mapStateToProps = state => ({
accounts: state.accountSelection.accounts, //accounts from redux store
});
export default connect(mapStateToProps)(SwitchAccount);
If you're using React Hooks, you can simply dispatch an action by using React.useEffect
React.useEffect(props.dispatchOnAuthListener, []);
I use this pattern for register onAuthStateChanged listener
function App(props) {
const [user, setUser] = React.useState(props.authUser);
React.useEffect(() => setUser(props.authUser), [props.authUser]);
React.useEffect(props.dispatchOnAuthListener, []);
return <>{user.loading ? "Loading.." :"Hello! User"}<>;
}
const mapStateToProps = (state) => {
return {
authUser: state.authentication,
};
};
const mapDispatchToProps = (dispatch) => {
return {
dispatchOnAuthListener: () => dispatch(registerOnAuthListener()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Same solution as Chris Kemp mentions above. Could be even more generic, just a canLift func not tied to redux?
interface Props {
selector: (state: RootState) => boolean;
loader?: JSX.Element;
}
const ReduxGate: React.FC<Props> = (props) => {
const canLiftGate = useAppSelector(props.selector);
return canLiftGate ? <>{props.children}</> : props.loader || <Loading />;
};
export default ReduxGate;
Using: Apollo Client 2.0, React-Router v4, React 16 (Fiber)
The answer selected use old React Router v3. I needed to do 'dispatch' to load global settings for the app. The trick is using componentWillUpdate, although the example is using apollo client, and not fetch the solutions is equivalent.
You don't need boucle of
SettingsLoad.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from "redux";
import {
graphql,
compose,
} from 'react-apollo';
import {appSettingsLoad} from './actions/appActions';
import defQls from './defQls';
import {resolvePathObj} from "./utils/helper";
class SettingsLoad extends Component {
constructor(props) {
super(props);
}
componentWillMount() { // this give infinite loop or no sense if componente will mount or not, because render is called a lot of times
}
//componentWillReceiveProps(newProps) { // this give infinite loop
componentWillUpdate(newProps) {
const newrecord = resolvePathObj(newProps, 'getOrgSettings.getOrgSettings.record');
const oldrecord = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
if (newrecord === oldrecord) {
// when oldrecord (undefined) !== newrecord (string), means ql is loaded, and this will happens
// one time, rest of time:
// oldrecord (undefined) == newrecord (undefined) // nothing loaded
// oldrecord (string) == newrecord (string) // ql loaded and present in props
return false;
}
if (typeof newrecord ==='undefined') {
return false;
}
// here will executed one time
setTimeout(() => {
this.props.appSettingsLoad( JSON.parse(this.props.getOrgSettings.getOrgSettings.record));
}, 1000);
}
componentDidMount() {
//console.log('did mount this props', this.props);
}
render() {
const record = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
return record
? this.props.children
: (<p>...</p>);
}
}
const withGraphql = compose(
graphql(defQls.loadTable, {
name: 'loadTable',
options: props => {
const optionsValues = { };
optionsValues.fetchPolicy = 'network-only';
return optionsValues ;
},
}),
)(SettingsLoad);
const mapStateToProps = (state, ownProps) => {
return {
myState: state,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators ({appSettingsLoad, dispatch }, dispatch ); // to set this.props.dispatch
};
const ComponentFull = connect(
mapStateToProps ,
mapDispatchToProps,
)(withGraphql);
export default ComponentFull;
App.js
class App extends Component<Props> {
render() {
return (
<ApolloProvider client={client}>
<Provider store={store} >
<SettingsLoad>
<BrowserRouter>
<Switch>
<LayoutContainer
t={t}
i18n={i18n}
path="/myaccount"
component={MyAccount}
title="form.myAccount"
/>
<LayoutContainer
t={t}
i18n={i18n}
path="/dashboard"
component={Dashboard}
title="menu.dashboard"
/>

Resources