react-router v4 dispatch redux action on page change - javascript

I would like an action to be dispatched and then intercepted by the reducers on every react-router page change.
My use case is: I have the page state persisted in redux store. The state is represented as {loading, finished}. When the user clicks a button, a request is made to the server and loading becomes true. When the response comes back positive loading becomes false and finished becomes true. When the user navigates away from the page, I want the state to be reset to its initial values (loading=false, finished=false).
The following answer is great, but not work on v4 anymore because onEnter onChange and onLeave were removed.
React Router + Redux - Dispatch an async action on route change?
Edit: The best solution would be something scalable. In the future there would be multiple such pages and the state for each of them should reset on page nagivation
Thank you!

You could create your history instance separately, and listen to that and dispatch an action when something changes.
Example
// history.js
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
history.listen(location => {
// Dispatch action depending on location...
});
export default history;
// App.js
import { Router, Route } from 'react-router-dom';
import Home from './components/Home';
import Login from './components/Login';
import history from './history';
class App extends React.Component {
render() {
return (
<Router history={history}>
<div>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
</div>
</Router>
)
}
}

v4 has the location object which could be used to check if the app has navigated away from the current page.
https://reacttraining.com/react-router/web/api/location

Related

How can I listen to route change in my main App component, using a custom history object?

I have a header that appears in 95% of pages in my site, so I mount it in my main App component. For the other 5% of pages though I need it to be gone.
I figured a good way to do it would be to change a state from true to false based on the current route, and that state would determine whether the header mounts or not.
at first I tried just using window.location.href as a useEffect dependency but that didn't seem to work. I've read about the option to listen to the location of the history, but I keep getting Cannot read property 'history' of undefined. I thing that perhaps it's because I am using a custom history component, or maybe because I try to do so in my main App component? Not sure.
This is my history object:
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
I use it in my Router like this:
<Router history={history}>
CONDITIONAL COMPONENT
<Switch>
...routes
</Switch>
</Router>
This is how I try to listen, where history is my custom history object
const MyHistory = useHistory(history)
useEffect(() => {
return MyHistory.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`)
})
},[MyHistory])
You are trying to use useHistory within a component that renders Router which is a Provider. In order for useHistory to Work it needs to have a Router higher up in the hierarchy.
So either you wrap the App component with Router like
export default () => (
<Router history={history}><App /><Router>
)
or since you define a custom history you can use it directly without using useHistory
useEffect(() => {
return history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`)
})
},[])
This is how I control every route change in my App. I created a component that listen to the pathname property given by useLocation
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function AppScrollTop() {
const { pathname } = useLocation();
useEffect(() => {
console.log(`You changed the page to: ${pathname}`)
}, [pathname]);
return null;
}
Then I put the component inside the <Router>
<BrowserRouter>
<AppScrollTop />
<Switch>
<Route path="/login" component={Login} />
</Switch>
</BrowserRouter>
I hope this helps.

What is the right way to elevate props in React?

I apologize if my phrasing is wrong in the title. I've recently gotten cookies going in my app. My Topnav component needs access to them, but I'm unsure how to get them there.
App.js -
import React, { Component } from 'react';
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Landing from './pages/Landing.js';
import LoginPage from './pages/LoginPage.js';
import Topnav from './pages/components/global/Topnav.js';
import './Global.css';
const App = props => (
<div>
<Topnav />
<Switch>
<Route exact path='/' component={Landing} />
<Route exact path='/login' component={LoginPage} />
</Switch>
</div>
);
export default App;
My login component grabs the cookie from express (fetch) and then does a <Redirect to='/' />
This loads up my Landing page, where I'm able to grab the cookie, but how do I get the cookie to the Topnav? I saw an answer to something like this on stack where it seems like App.js grabs the cookie and passes it as a props to the components, but I don't see how it could if it never refreshes. I've thought about forcing an entire window refresh (which does work for Topnav when I do a refresh manually), but I've also seen answers here that say don't do that.
Use Context
You need to use the new context hook from react.
Create a context
This is a context that you can access around your app.
const MyContext = React.createContext(defaultValue);
Make a provider
Wrap the provider around your main app
<MyContext.Provider value={/* some value */}>
Access the context at the point at which you get the cookies
Use this in both your login and top nav to use the value from the context
const value = useContext(MyContext);
There are multiple ways to approach this.
Probably a beginner friendly one.
When your login Component does the login successfully you need to signal to the App Component about it probably using a onLoginSuccessful which can then read the cookie and do a setState with it and use this component state value in the props to your Topnav and Landing Component

React Router Dom (4) Programmatically Navigate

I am working on a React app that is 'remote controlled' and am trying to connect the app's tab navigation via Pusher. I am using react-router-dom and have set up my connection to Pusher and the channels it listens to in the componentDidMount. I need to have the app change URL navigation each time a message is returned but cannot figure out what the correct way to 'push' it is. I have google many threads about programmatically navigating react-router-dom and have tried this.history.push(), this.props.history.push() and it's always throwing an error that history is undefined. The listeners reside in the same Component as the routing itself.
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import axios from 'axios';
import Pusher from 'pusher-js';
import Component1 from './Component1';
import Component2 from './Component2';
export default class AppRouter extends Component{
componentDidMount() {
const pusher = new Pusher('8675309', { cluster: 'us2', encrypted: true });
const nav_channel = pusher.subscribe('nav');
nav_channel.bind('message', data => { this.props.history.push(data.message) });
}
...
render(){
return(
<Router>
<Switch>
<Route path='/path1' render={() => <Component1 navigate={this.handleNavChange} />} />
<Route path="/path2" render={() => <Component2 navigate={this.handleNavChange} />} />
</Switch>
</Router>
)
}
}
I need the app to change the URL or the routing each time the message is received from another connected app but I cannot figure out how to make react-router-dom (v4) do it within the same component as the Router itself. Any information or pointing me to a resource would be highly appreciated.
You need to use the withRouter decorator from React-Router to get access to match, location, and history.
import { withRouter } from 'react-router'
Wrap your component when exporting
export default withRouter(yourComponent)
Then you will have access to history from props.
https://reacttraining.com/react-router/core/api/withRouter
To add on to andrewgi's answer:
After using withRouter() on your component, OR a parent component,
try the following code in your component:
static contextTypes = {
router: PropTypes.object,
location: PropTypes.object
}
These should be defined, you can try console.log(this.context.router, this.context.location)
Then, to navigate programmatically, call
this.context.router.history.push('/someRoute');
Note: the context API is not stable. See Programmatically navigate using react router and https://reactjs.org/docs/context.html#why-not-to-use-context.

ReactJS + Redux: How to directly navigate to a page (skip login) if token is in local storage?

Currently in my root component, it is set to go directly to the Login page as default. But I would like to set it up where it checks to see if a token already exists in the local storage, and if it does, skip the Login page and navigate directly to the Home page.
With the following code I have set up, it navigates to the Home page but for a split second the Login page appears before navigating to the Home page.
How can I go directly to the Home page without the Home page showing up at all?
Here is the code:
import React from 'react';
import {render} from 'react-dom';
import configureStore from '../redux/store'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'
import cookie from 'react-cookie';
import actions from '../redux/actions'
import Login from '../components/Login'
import App from '../components/App'
import Home from '../components/Home'
let store = configureStore(initialState)
//Takes the token from local storage and set to const token
const token = cookie.load('token');
//This checks to see if the token exists in the local storage, and if it does, it enters the if statement and goes directly to the Home component.
if(token) {
window.location.href = 'http://localhost:3000/#/App'
}
render(
<div>
<Provider store={store}>
<Router history={hashHistory}>
<Route
component={Login}
path='/'
/>
<Route
component={App}
path='App'
>
<IndexRoute
component={Home}
/>
</Route>
</Router>
</Provider>
</div>,
document.getElementById('app')
)
I would create a root component called LoginCheck, in this component you would use the componentWillMount lifecycle hook to determine whether a user is logged in or not. If the user is logged in, it would continue to the correct page, otherwise it would redirect to the login page.
Hope this helps.
Hope it helps.
import {browserHistory} from 'react-router';
export class Home extends React.Component{
constructor(props, context)
{
super(props, context);
}
componentWillMount() {
if(token)
{
browserHistory.push("YOUR AUTHENTICATED PAGE");
}
}
}
you can write something like this in component will mount
import { push } from 'react-router-redux';
//login component
componentWillMount(){
if(token){
this.props.dispatch(push("/home"));
}
}
Explanation from Documentation React-Router-Redux
What if I want to issue navigation events via Redux actions?
React Router provides singleton versions of history (browserHistory
and hashHistory) that you can import and use from anywhere in your
application. However, if you prefer Redux style actions, the library
also provides a set of action creators and a middleware to capture
them and redirect them to your history instance.
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { routerMiddleware, push } from 'react-router-redux'
// Apply the middleware to the store const middleware =
routerMiddleware(browserHistory) const store = createStore(
reducers, applyMiddleware(middleware) )
// Dispatch from anywhere like normal. store.dispatch(push('/foo'))

React router browser back button isn't working

I'm trying to create a multi step form using React JS and react-router.
The form step is changed with state. If I click on the button next the state step is incremented and the next step is shown.
But this way if I'm on for example third step and I click on the back in the browser I'm redirected to the home page instead of a previous step.
My main component is something like this :
componentWillMount(){
console.log(this.props.params.id);
}
onButtonClick(name, event) {
event.preventDefault();
switch (name) {
case "stepFourConfirmation":
if(this.validation("four")) {
this.props.actions.userRegistrationThunk(this.state.user);
this.setState({step: 5, user: {}});
}
break;
case "stepTwoNext":
if(this.validation("two")) {
this.setState({step: 3});
this.context.router.push("stepThree");
}
break;
case "stepThreeFinish":
this.setState({step: 4});
break;
default:
if(this.validation("one")) {
this.setState({step: 2});
this.context.router.push('stepTwo');
}
}
}
On every button click I push the parameter to the url and change the step. When I click next it's working perfect. In componentWillMount I'm trying to get the parameter from the url and I would than decrement the step depending on the parameter.
But only when the first step is loaded I se stepOne in the console. If I click on next I get [react-router] Location "stepTwo" did not match any routes
My routes.js file is as follows :
import React from 'react';
import {IndexRoute, Route} from 'react-router';
import App from './components/App';
import HomePage from './components/HomePage';
import Registration from './components/registration/RegistrationPage';
import UserPage from './components/user/userHome';
import requireAuth from './common/highOrderComponents/requireAuth';
import hideIfLoggedIn from './common/highOrderComponents/hideIfLoggedIn';
import PasswordReset from './common/passwordReset';
const routes = (
<Route path="/" component={App}>
<IndexRoute component={HomePage}/>
<Route path="registration/:id" component={hideIfLoggedIn(Registration)}/>
<Route path="reset-password" component={PasswordReset} />
<Route path="portal" component={requireAuth(UserPage)} />
</Route>
);
export default routes;
Any advice how to solve it?
Try pushing an absolute path. Instead of
this.context.router.push("stepTwo");
try
this.context.router.push("/registration/stepTwo");
Additionally you have a problem with your lifecycle methods. componentWillMount is only called once. When you push the next route, the component is not mounted again, so you don't see the log with the next param. Use componentWillMount for the initial log, componentWillReceiveProps for the next ones:
componentWillMount() {
console.log(this.props.params.id);
}
componentWillReceiveProps(nextProps) {
console.log(nextProps.params.id);
}
Btw: here is a discussion on github. The creators state, that react router is not supporting relative paths

Categories

Resources