React-router doesn't sync history and Redux store - javascript

I am trying to get react-router and redux to work together.
When my component mounts, I make an ajax-call and until this one finishes I want to show a loading-message. Once the ajax-call returns successfully, I reroute to the correct page.
However, whenever I change routes programmatically, react-router doesn't update the shown page. Also, I don't want to use the browser's history but an internal one (memory history) and can't find any examples of this working together with redux.
import React, {Component, PropTypes} from 'react';
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, createMemoryHistory, useRouterHistory, routerReducer } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
let appHistory;
export default class Transfer extends React.Component {
componentWillMount(){
myAjaxCall(function(){
// Reroute
appHistory.push('/projectrepresentation'); // changes a route change in the router, but doesn't display /projectrepresentation
})
}
constructor(){
const reducers = combineReducers({
routing: routerReducer
})
store = createStore(reducers);
// Create an enhanced internal (!) history that syncs navigation events with the store
let createAppHistory = useRouterHistory(createMemoryHistory);
appHistory = createMemoryHistory();
history = syncHistoryWithStore(appHistory, store);
}
render () {
return <Provider store={store}>
<Router history={history}>
<Route path="/" component={() => <div>Loading</div>}>
<Route path="projectrepresentation" component={() => <div>Project Representation</div>}/>
<Route path="export" component={() => <div>Export</div>}/>
</Route>
</Router>
</Provider>
}
}

Related

How do you use react-router-dom to redirect user when token has expired?

I have a React application that accesses a Flask API. To access some API routes, the user needs to log in. I am using Axios to do the requests. Then, he receives a token which is stored in the local storage. When this token expires and the user makes another request, I want to redirect him to the login page. However, I don't know how I would do it.
I am treating API request errors with Axios response interceptor. It removes the token from the local storage and then should redirect the user to the login page. Since I am using functional components, I could not find an example that fits well (besides downloading another package called history).
I have tried to use the 'useHistory' hook and Redirect from react-router-dom (with a proper BrowserRouter set up), but it doesn't work.
api.js
import axios from "axios"
import { RemoveAuth } from "./Auth"
export const api = axios.create({
baseURL: "http://localhost:5000/api/",
timeout: 15000,
})
// more code
api.interceptors.response.use(null, (error) => {
if(error.response.status === 401){
RemoveAuth();
}
return error;
});
Auth.js
import { useHistory } from "react-router-dom"
export const RemoveAuth = () => {
let history = useHistory()
localStorage.clear();
history.push('/login')
}
routes.js
import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import PrivateRoutes from "./PrivateRoutes";
import Dashboard from "../pages/dashboard";
import Login from "../pages/login";
import Logout from "../pages/logout";
const Routes = () => (
<BrowserRouter>
<Switch>
<PrivateRoutes exact path="/dashboard" component={Dashboard} />
<PrivateRoutes exact path="/logout" component={Logout} />
<Route exact path="/login" component={Login} />
</Switch>
</BrowserRouter>
);
PrivateRoutes.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthLogin } from "../services/Auth";
const PrivateRoutes = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={() => (AuthLogin() ? <Redirect to="/login" /> : <Component />)}
/>
);
export default PrivateRoutes;
Thanks in advance for the help!
The simplest thing to do is to create your own history object. Something like this:
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
Then in your provider pass in your custom history object:
import { Router } from 'react-router-dom'
import history from './utils/history'
ReactDOM.render(
<Router history={history}>
<App />
</Router>
document.getElementById('root')
);
This allows you to utilize your history in non-component code. Just import your history object into your Auth.js file and use it:
import { history } from './utils/history'
export const RemoveAuth = () => {
localStorage.clear();
history.push('/login')
}
As an added bonus, now your history lives in a place that is easily mock-able, so creating testing around it is more straightforward. You can find more information about creating your own custom history object in the docs.

React Router v5: history.push() changes the address bar, but does not change the page

I am trying to redirect a user to a new page if a login is successful in my React app. The redirect is called from the auth service which is not a component. To access the history object outside of my component I followed this example in the React Router FAQ. However, when I call history.push('/pageafterlogin'), the page is not changed and I remain on the login page (based on my Switch I would expect to end up on the 404 page). The URL in the address bar does get changed to /pageafterlogin but the page is not changed from the login page. No errors appear in the console or anything else to indicate my code does not work.
How can I make history.push() also change the page the user is on?
// /src/history.js
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
// /src/App.js
...
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import history from './history';
function App() {
return (
<Router history={history}>
<Switch>
<Route path="/" exact component={HomePage} />
<Route path="/login" exact render={() => <FormWrapper><LoginForm /></FormWrapper>} />
<Route render={() => <h1>404: not found</h1>} />
</Switch>
</Router>
);
}
export default App;
// src/services/auth.service.js
import axios from 'axios';
import history from '../history';
const API_URL = '...';
class AuthService {
login(username, password) {
return axios.post(API_URL + 'login', {
username,
password
}).then(res => {
if (res.status === 200) {
localStorage.setItem('user', JSON.stringify(res.data));
history.push('/pageafterlogin');
}
});
}
}
Instead of using BrowserRouter, use Router from react-router-dom
You could see the example here
import { Router, Route, Switch, useHistory, create } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import React from 'react';
const history = createBrowserHistory();
export default function App() {
return (
<Router history={history}>
<Switch>
<Route path="/" exact component={() => <h1>HomePage</h1>} />
<Route path="/login" exact component={Login} />
<Route render={() => <h1>404: not found</h1>} />
</Switch>
</Router>
);
}
function Login() {
React.useEffect(() => {
history.push('/pageafterlogin')
}, [])
return <h1>Login page</h1>
}
If you are looking for a solution to this in 2022 and are using React V18+,
the solution is that React v18 does not work well with react-router-dom v5.
I have not tried with react-router-dom v6 yet, but downgrading to React V17 solved the issue for me.
I removed StrictMode and it solved the problem
import { Router } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import React from 'react';
const history = createBrowserHistory();
export default function MyApp() {
return (
<Router history={history}></Router>
);
}
I had the same problem when I hadn't specified the vesion of 'history'. You need to use a 4.x version with Router 5.x. For example, I use React v18, history v4.7.2 and react-router-dom v5.3.3 and it works fine.
Try
npm i history#4.7.2

Redux load state async and using with axios

I am new to React and Redux and currently I am working on React project and have some questions about Redux integration.
1. Load state asynchronously
I have index.js file which is entry point of my project:
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router } from 'react-router-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
)
Here I import store.js:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(thunk)
)
export default store
And App.js:
import React, { Component } from 'react'
import { Route } from 'react-router-dom'
import UserRoute from './components/UserRoute'
import GuestRoute from './components/GuestRoute'
import Menu from './components/Menu'
import LoginPage from './components/pages/LoginPage'
import SignupPage from './components/pages/SignupPage'
import HomePage from './components/pages/HomePage'
import ProfilePage from './components/pages/ProfilePage'
import GroupsPage from './components/pages/GroupsPage'
class App extends Component {
render() {
return (
<div>
<Menu />
<Route exact path="/" component={HomePage} />
<GuestRoute exact path="/login" component={LoginPage} />
<GuestRoute exact path="/signup" component={SignupPage} />
<UserRoute exact path="/profile" component={ProfilePage} />
<UserRoute exact path="/groups" component={GroupsPage} />
</div>
)
}
}
export default App
I cut some code to make it more readable for you. Notice UserRoute and GuestRoute components. UserRoute allows only authenticated users to navigate to the page and GuestRoute does the opposite. I have user token saved in localStorage, then I use it to get user data from server. I want to get response before rendering my App component. If I don't wait response and my location is /profile for example, then UserRoute will redirect me to main page. To avoid redirect I can do this:
getState().then(() => {
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
)
})
But is it good way to load state before rendering? Or there are other "better" ways to do this?
2. Importing store in other files
How can I get current state in axios request? Is it OK to export store in store.js and then import it in api.js file:
import axios from 'axios'
import store from './store'
const client = axios.create({
baseURL: 'http://localhost:3001',
headers: { 'Content-type': 'application/json' }
})
// Used to set current JWT token(part of third question)
export function setAuthorizationHeader(token = '') {
if (token) {
client.defaults.headers.common['Authorization'] = `Bearer ${token}`
} else {
delete client.defaults.headers.common['Authorization']
}
}
export default {
user: {
fetch() {
return client.get('/user').then(res => res.data)
},
// other api calls which use current state of store
}
}
3. Not pure action creators
This question is about action creators. I have following action creators:
import api, { setAuthorizationHeader } from '../api'
export function setUser(user) {
return { type: 'SET_USER', user }
}
export function setUserToken(token) {
return { type: 'SET_USER_TOKEN', token }
}
export const login = credentials => async dispatch => {
const { user, token } = await api.user.login(credentials)
// TODO: refactor this
setAuthorizationHeader(token)
dispatch(setUserToken(token))
dispatch(setUser(user))
}
When I get response from server I set Authorization header for axios client. Is it OK that they are not pure functions?
Also you can look at my repository to get more understanding how code works.

Redux not working with React-router

I'm using the following:
react v16.2.0,
react-redux v5.0.7,
react-router-dom v4.2.2,
redux v3.7.2
What I am trying to achieve is to update some props from the Auth component, and when the user navigates to /user (Which loads the Userpage component), the modified props should be displayed.
Here is a simplified version of my code:
In App.js:
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Store from './components/Store';
import Home from './components/Home';
import Auth from './components/auth';
import Userpage from './components/Userpage';
import { BrowserRouter as Router, Route } from 'react-router-dom';
class App extends Component {
render() {
return (
<Provider store={Store}>
<Router>
<div>
<Route exact path="/" component={Home}/>
<Route path="/login" component={Auth}/>
<Route path="/user" component={Userpage}/>
</div>
</Router>
</Provider>
);
}
}
export default App;
In Store.js:
import { createStore } from 'redux';
const reducer = (state,action) => {
if(action.type == 'TEST'){
return Object.assign({},state,{test:action.payload.test});
} else
return state;
}
export default createStore(reducer,{
test:'DOES NOT WORK',
})
In Auth.js :
import React from 'react';
import { connect } from 'react-redux';
import Userpage from './Userpage';
class Auth extends React.Component {
componentWillMount() {
this.props.update('TEST',{test:'WORKS'});
}
render() {
return(
<div>
<Userpage/>
</div>
);
}
}
export default connect(
(store) => {
return store;
},
(dispatch) => {
return {
update:(dispatchType, dispatchPayload) => {
dispatch({type:dispatchType,payload:dispatchPayload});
}
}
}
)(Auth);
In Userpage.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Userpage extends Component{
componentDidMount(){
console.log(this.props.test);
}
render(){
return null;
}
}
export default connect(
(store) => {
return store;
}
)(Userpage);
Now, when I navigate to /login, the store is updated and test is set to "WORKS". But if I navigate to /userpage, console.log(this.props.test) prints "DOES NOT WORK", instead of the updated prop.
Just to test, I included Userpage in the render function of Auth and it console logs "WORKS" correctly.
So why is the redux store apparently being reset to default values on navigation to another page using react-router? How can I fix this?
Found the solution. Apparently the whole application re-renders when you manually navigate (by typing in the URL in the browser), hence resetting the redux store.
Using Link as mentioned in react-router docs or redirecting using this.props.history.push('/user') works without any issues.

Access server props from within redux?

I'm trying to use redux, react-engine, and react-router.
The issue or question I have is that react-engine provides an object of props that come from the server. How do I access these props from within my ProvidedApp?
ProvidedApp.js
import React from 'react'
import { connect, Provider } from 'react-redux'
import App from './app'
import { mapStateToProps, mapDispatchToProps, store } from './redux-stuff'
// Connected Component
let ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App)
let ProvidedApp = () => (
<Provider store={store}>
<ConnectedApp/>
</Provider>
)
export default ProvidedApp
Routes.js
import React from 'react'
import { Router, Route } from 'react-router'
import Layout from './views/Layout'
import App from './views/ProvidedApp'
module.exports = (
<Router>
<Route path='/' component={Layout}>
<Route path='/app' component={App} />
<Route path='/app/dev' component={App} />
</Route>
</Router>
)
I also think my configuration is a little wonky, I couldn't get Provider working any other way. If theres a way to have Provider wrap the Router I'd love to get that working.
Here's some code of what it looks like when I move Provider above Router
ConnectedApp.js
import React from 'react'
import { connect, Provider } from 'react-redux'
import App from './app'
import { mapStateToProps, mapDispatchToProps} from './redux-stuff'
let ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App)
export default ConnectedApp
Routes.js
import React from 'react'
import { Provider } from 'react-redux'
import { Router, Route } from 'react-router'
import { store } from './redux-stuff'
import Layout from './views/Layout'
import App from './views/ConnectedApp'
module.exports = (
<Provider store={store}>
<Router>
<Route path='/' component={Layout}>
<Route path='/app' component={App} />
</Route>
</Router>
</Provider>
)
I get this error:
Could not find "store" in either the context or props of "Connect(App)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(App)".
Update
I found that I can access from he code in my first example within ProvidedApp. But I have no clue how I'm supposed to pass it into Redux.
let ProvidedApp = (props) => {
console.log(props)
return (
<Provider store={store}>
<ConnectedApp/>
</Provider>
)
}
Seems like I need to wrap the reducer and store and pass in the ServerProps to the default state like this.
let getDefaultState = (serverProps) => {
return {
'appName': serverProps.appName
}
}
let getReducer = (serverProps) => {
let defaultState = getDefaultState(serverProps)
return (state = defaultState, action) => {
}
}
let getStore = (serverProps) => {
let reducer = getReducer(serverProps)
return store = createStore(reducer)
}
let ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App)
let ProvidedApp = (serverProps) => {
return (
<Provider store={getStore(serverProps)}>
<ConnectedApp/>
</Provider>
)
}
This is super ugly :/

Categories