I am creating a React application using create-react-app, react-redux, react-thunk, react-router.
I am setting a localstorage variable "current_user" after authenticating so that I can setup my Restricted Routers.
I am using Axios to make the ajax requests, and In my initial index.js file I have setup a global Axios Interceptor so that if any ajax call returns 401, it automatically clears the local storage "current_user". I would like it to also redirect to /Login.
I'm able to do this the old fashioned way (ie window.location) however, is there a way to set this global function to redirect from anywhere in the application to the login page?
The global interceptor:
index.js
import "materialize-css/dist/css/materialize.min.css";
import "jquery/dist/jquery.min.js";
import "materialize-css/dist/js/materialize.min.js";
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import reduxThunk from 'redux-thunk';
import axios from "axios";
import App from "./components/App";
import reducers from './reducers';
const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
axios.interceptors.response.use(undefined, function (error) {
if(error.response.status === 401) { //Setup Global Config
localStorage.setItem('current_user', null);
//Would like to redirect to /Login here
}
});
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
You can try something like this :
import createHistory from 'history/createBrowserHistory';
const history = createHistory({
window.location.pathname,
});
// ...
if(error.response.status === 401) {
// ...
history.push('/login');
}
Here you can find another approach to redirect your user if you don't have the token in the localStorage : https://github.com/strapi/strapi-examples/blob/master/login-react/react-login-front-end-app/app/containers/WithAuth/index.js
Just Use : return <Redirect to="/login" />. That should do.
Related
I have a React application which wished to add an Azure login authentication.
The original code calling the App.js in index.js without Azure AD is as below:
import React from 'react';
import ReactDOM from 'react-dom';
import 'index.css';
import App from 'App';
import * as serviceWorker from 'serviceWorker';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducer from 'store/reducer'
//, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
{/* <React.StrictMode> */}
<App />
{/* </React.StrictMode> */}
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
After adding Azure AD:
import React from 'react';
import ReactDOM from 'react-dom';
import 'index.css';
import App from 'App';
import * as serviceWorker from 'serviceWorker';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import reducer from 'store/reducer'
import { AzureAD } from 'react-aad-msal';
import { authProvider } from './authProvider';
ReactDOM.render(
<AzureAD provider={authProvider} forceLogin={true}>
<App />
</AzureAD>,
document.getElementById('root'),
);
serviceWorker.unregister();
The above code can pass my organization Azure login. However, it would show a blank page after the login had been passed, even I tried to change <App /> to <h1>Hello World</h1> would still be blanked.
Do you have any insights on this? Many thanks!
Reference:
https://medium.com/#pavelray/connect-your-react-app-with-azure-ad-using-3ddd39223d27
https://www.npmjs.com/package/react-aad-msal
I've followed the doc your provided and tested in my side, it worked indeed, but I made some changes which I think is wrong. In my opinion, it seems to have something wrong with the redirect url.
This is my config, pls note I used 'http://localhost:3000' rather than 'https' which is mentioned in the doc. And of course, I set the same redirect url in azure portal. Then it worked, I can redirect to the home page after login.
import { MsalAuthProvider, LoginType } from 'react-aad-msal';
// Msal Configurations
const config = {
auth: {
authority: 'https://login.microsoftonline.com/common',
clientId: '<client-id>',
redirectUri: 'http://localhost:3000'
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
// Authentication Parameters
const authenticationParameters = {
scopes: [
// '<property (i.e. user.read)>',
// 'https://<your-tenant-name>.onmicrosoft.com/<your-application-name>/<scope (i.e. demo.read)>'
'user.read'
]
}
// Options
const options = {
loginType: LoginType.Popup,
tokenRefreshUri: window.location.origin + '/auth.html'
}
export const authProvider = new MsalAuthProvider(config, authenticationParameters, options)
Finally, I found a doc which said Microsoft is currently in process of building an official #azure/msal-react library which will replace react-aad-msal, so if you prepare to change your mind, I think the samples in this page could help you. It contains the one uses #azure/msal-react.
So I have been creating an application where a user needs to log into firebase using google authentication. I am using redux, react-redux, react-redux-firebase, redux-firestore, and redux-thunk. I am able to successfully log the user into firebase with the google authentication. I now want to use firestore in order to have a collection of all the users. I have looked at the documentation for redux-firestore and the method of getting/manipulating is a little different. I have tried using the documentation, but I cannot get the functions to work with redux-firestore.
Here is the action
export const signIn = () => (
dispatch,
getState,
{getFirebase, getFirestore}) => {
const firebase = getFirebase();
const firestore = getFirestore();
firebase.auth().signInWithPopup(provider).then(function(result) {
if(result.credential) {
firestore.get({collection: 'users', doc: result.user.uid}).then(function(doc) {
if(!doc.exists){
console.log("new!")
firestore.add(
{collection: 'users', doc: result.user.uid},
{name: firebase.auth.currentUser.displayName});
} else{
console.log("old!")
}
})
}
}).catch((err) => {
})
};
And here is my setup in index.js for the src folder
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {BrowserRouter} from 'react-router-dom';
import {createStore, applyMiddleware, compose} from 'redux';
import {Provider} from 'react-redux';
import allReducers from './reducers';
import thunk from 'redux-thunk';
import firebase from './Firebase';
import {firebaseConfig} from './Firebase'
import {createFirestoreInstance, getFirestore, reduxFirestore} from 'redux-firestore';
import {ReactReduxFirebaseProvider, getFirebase} from 'react-redux-firebase';
const store = createStore(
allReducers,
compose(
applyMiddleware(thunk.withExtraArgument({getFirebase, getFirestore})),
reduxFirestore(firebaseConfig)
));
const rrfConfig = {
userProfile: 'users',
useFirestoreForProfile: true
};
const rrfProps = {
firebase,
config: rrfConfig,
dispatch: store.dispatch,
}
ReactDOM.render(
<Provider store={store}>
<ReactReduxFirebaseProvider {...rrfProps}>
<BrowserRouter>
<App />
</BrowserRouter>
</ReactReduxFirebaseProvider>
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
I know that I have not used createFirestoreInstance in this code, but I was playing around with it.
If anyone could tell me how to get this working, I would appreciate it.
Thanks!
Quick update:
I have figured out how to at least write to firestore using this code
const userRef = firebase.firestore().collection('users').doc(result.user.uid);
userRef.get().then(function(doc) {
if(!doc.exists){
userRef.set({name: result.user.displayName});
}
})
This is not the best (or maybe the right solution), but it does work. It is not using redux-firestore, but is there a better way?
If you're using React, use react-redux-firebase. There's no need for these many complication and the code looks much neater and simpler. Authentication, firestore and all other firebase features works out of the box with just small amount of code. They also comes with React hooks like useFirebase() and useFirestore() instead of you needing to write them on your own.
react-redux-firebase is built on top of redux-firebase and provides all the things you would need in React.
If your app only uses firebase, I would even recommend you use just plain Redux without Redux Thunk or Redux Saga.
Updating this question to use connected-react-router instead of react-router-redux since it is not compatible with react-router v4.
I can't seem to get my routing working when dispatching an action. I suspect it's because I'm using sagas which aren't being configured properly.
I have a saga:
import { call } from 'redux-saga/effects'
import { push } from 'connected-react-router'
//...
yield call(push, '/dashboard')
The push function doesn't redirect the browser to the specified path despite the redux logs in webdev tools showing that the action was successfully dispatched.
The top level index.js file looks like:
import createSagaMiddleware from 'redux-saga'
import rootSaga from './redux/sagas'
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import logger from 'redux-logger'
import App from './App'
import registerServiceWorker from './registerServiceWorker'
import rootReducer from './redux/modules'
import { applyMiddleware, compose, createStore } from 'redux'
import { createBrowserHistory } from 'history'
import { routerMiddleware, connectRouter } from 'connected-react-router'
const history = createBrowserHistory()
const sagaMiddleware = createSagaMiddleware()
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(
connectRouter(history)(rootReducer),
composeEnhancer(
applyMiddleware(
sagaMiddleware,
routerMiddleware(history),
logger
)
)
)
sagaMiddleware.run(rootSaga)
const render = () => {
ReactDOM.render(
<Provider store={store}>
<App history={history} />
</Provider>,
document.getElementById('root')
)
}
render()
registerServiceWorker()
The App.js file containing the root component has:
import { ConnectedRouter } from 'connected-react-router'
import { Route, Switch, Redirect } from 'react-router-dom'
const App = ({ history }) => {
return (
<ConnectedRouter history={history}>
<Switch>
{ routes }
</Switch>
</ConnectedRouter>
)
}
export default App
What's missing from this setup to make it work?
Dependency versions:
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"connected-react-router": "^4.3.0"
Unlike history's push method (which is an impure function), connected-react-router's push is an action creator and its result (action) must be dispatched to trigger a navigation.
To do so in redux-saga you have to use put, not call.
call creates a call effect.
When yielded, it simply executes given function with given arguments and returns a result. It is a good fit for (but not limited by) impure function calls (e.g. network request), by decoupling us from a direct execution of a function.
put creates a dispatch effect.
When yielded, it dispatches passed in action object. Thus, decoupling your code only from a direct call of dispatch, not action creator (which should be pure by design).
So, in your case, the solution would look like:
yield put(push('/dashboard'))
P.S: the same applies to react-router-redux's push
you need to wire up the router's middleware, e.g.:
import { browserHistory } from 'react-router'
import { routerMiddleware } from 'react-router-redux'
const sagaMw = createSagaMiddleware()
const routerMw = routerMiddleware(browserHistory)
const middleware = applyMiddleware(sagaMw, routerMw, logger)
Sagas are implemented as Generator functions that yield objects to the
redux-saga middleware
So your Saga should export a Generator function:
import { call } from 'redux-saga/effects'
import { push } from 'connected-react-router'
//...
export function* rootSaga() {
return yield call(push, '/dashboard')
}
And rootSaga should be registered with the sagaMiddleware:
import { rootSaga } from './redux/sagas';
...
sagaMiddleware.run(rootSaga)
...
Reference: https://redux-saga.js.org/docs/introduction/BeginnerTutorial.html
What seems to work for me is to use withRoter (dont know if it is correct way or not):
export default withRouter(compose(withConnect)(App)); //wrapped in withRouter
Or
export default compose(
withRouter,
withConnect,
)(App);
And in redux-saga:
yield put(push('/newpage'));
I use react-boilerplate, https://github.com/react-boilerplate/react-boilerplate.
I don't know if it is correct or not but this way the route changes in url and I get to the new route. If i don't use withRouter the route changes in url and nothing more happens...
I found solution here https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
Would like to have someone opinion on that. Folks at https://github.com/react-boilerplate/react-boilerplate maybe?
I somehow managed to get it work, but i think it is not good solution.
my main file is:
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, hashHistory } from 'react-router';
import { Provider } from 'react-redux';
import routes from './router/routes';
import store from './store.js';
ReactDOM.render(
<Provider store={ store }>
<Router routes={ routes } history={ hashHistory } />
</Provider>,
document.getElementById('wrap')
);
my store.js is:
import { createStore } from 'redux';
import reducer from './reducers/index';
import initialState from './reducers/initial-state';
import connection from './lib/connection';
const store = createStore(reducer, initialState);
connection(store);
export default store;
and connection is:
import AppActions from '../actions/AppActions';
import io from 'socket.io-client';
export default function (store) {
const mainsocket = io.connect('$socketurl');
mainsocket.store = store;
mainsocket.actions = AppActions;
var socket;
mainsocket.on('namespaceinitialized', function(data) {
socket = io.connect("$socketurl/$namespace", {'autoConnect': false,'forceNew': true});
socket.connect();
socket.store = store;
socket.actions = AppActions;
socket.on('connect',function (data) {
//need store and actions
});
});
}
As you see i am using 2 connections, first for namespace initialize and then second (this is not important in question).
I dont like this "dirty" solution where i am attaching store and actions to mainsocket object and then passing them from mainsocket to socket, in order to have access to app actions on socket messages, and the question is what is right way?
Also in this code socket connects before app actions and store have initialized, i would like to socket connection be last thing but i don't know where to put it then?
I must say that i am don't writing my own application but adding functionality to existing open source one.
thank you.
Redux advocates the use of a single store with a single state. However, with react-router you get multiple pages each having their own top-level root component.
How should one go about wiring up redux with a react-router app that has multiple pages? React-router 1.0 no longer lets you pass props to the routes so making the router the top level component that contains the state for all the pages is no longer possible nor is it feasible.
If you are using redux + react-router I would highly recommend using redux-router as well - this will allow you to keep route information in your store - I usually have the following setup.
redux-router: ^1.0.0-beta3 /
react-router": ^1.0.0-rc1 /
redux: "^3.0.2 /
react: "^0.14.0
//index.js [entry point]
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route } from 'react-router';
import { Provider } from 'react-redux';
import createStore from './utils/create-store';
import routes from './bootstrap/routes';
import { ReduxRouter } from 'redux-router';
const store = createStore(routes);
ReactDOM.render(
<Provider store={store}>
<ReduxRouter>
{routes}
</ReduxRouter>
</Provider>,
document.getElementById('root')
);
// create-store.js
import { applyMiddleware, createStore, combineReducers, compose } from 'redux';
import * as reducers from '../reducers/index';
import promiseMiddleware from './promise-middleware';
import { routerStateReducer, reduxReactRouter } from 'redux-router';
import history from './history'
/**
* Sets up the redux store. Responsible for loading up the reducers and middleware.
*
* #param routes
*/
export default function create(routes) {
const composedReducers = combineReducers({
router: routerStateReducer,
...reducers
});
const finalCreateStore = compose(applyMiddleware(promiseMiddleware),
reduxReactRouter({
routes,
history
}))(createStore);
let store = finalCreateStore(composedReducers);
return store;
}
// routes.js
import React from 'react';
import { Route } from 'react-router';
import App from './app';
module.exports = (
<Route component={App}>
...all your routes go here
</Route>
);
// app.js
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
export default class App extends React.Component {
render() {
const { props: { children } } = this;
return (
<div className="app-container">
{children}
</div>
);
}
}
So as you can see there is one higher order component that wraps the rest of your routes
Redux-router is no longer compatible with react-router v4, and the above solution will not work.
As such, I suggest utilzing redux-little-router. Unlike redux-router it is not dependent on react-router, but instead a replacement for it.
With it you can do things like:
Push to new routes from your async actions:
export function navigateAbout() {
return (dispatch) => {
dispatch(push('/about'))
}
}
See your router details (current path, querystring and params) inside redux-dev-tools, and access those details from you store like you would for any other prop:
function mapStateToProps(state) {
return {
string: state.router.query.string
}
}
PS. You can refer to or use this react boilerplate with redux-little-redux already implemented