React Router + Redux - Dispatch an async action on route change? - javascript

I have a universal react app that's using redux and react-router.
I have several routes as follows:
/2016
/2015
/2014
/2013
etc.
Each route requires data from an API. Currently, i have the <Link> elements in the Navigation component dispatch an async action onClick, which populates the store with data from the API for that route.
For MVP, i'm just overwriting the post: {} contents in the store with the new post contents when the route changes, that way we get any new content that was on the API.
I've realise that having the action dispatchers on the <Link> buttons isn't optimal, as hitting the back button does not re-trigger the action dispatch to get the content for the previous route.
Is there a way to get React Router to trigger the dispatch action anytime a route change occurs? (Limiting it to listen to a specific set of routes would be a bonus).
I realise i should be getting the history from the store, but for now, it's easier to hit the API again by triggering an action dispatch in order to get the new content.
Cheers.

The 'lifecycle' hook onEnter and onChange has been removed in React-router 4 which makes most of the other answers to this question out-dated.
Whilst I recommend you to use your components lifecycle methods to achieve your goal, here is an answer to your question which works on React-router 4.
What works today is listen to the history change using History library created by the developers of React router themself and dispatch async actions from there.
// history.js
import createHistory from "history/createBrowserHistory"
const history = createHistory()
// Get the current location.
const location = history.location
// Listen for changes to the current location.
const unlisten = history.listen((location, action) => {
//Do your logic here and dispatch if needed
})
export default history
Then import the history in your application
// 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 Component {
render() {
return (
<Router history={history}>
<div>
<Route exact path="/" component={Home} />
<Route path="/login" component={Login} />
</div>
</Router>
)
}
}
Source: History library
React router docs

Yeah React Router has onEnter and onLeave hooks. You could build your routes to take your store instance, so you can access it in those helpers:
const createRoutes = (store) => {
const fetchPosts = () => store.dispatch({
types: ['FETCH_POSTS', 'FETCH_POSTS_SUCCESS', 'FETCH_POSTS_FAIL',
url: '/posts'
});
return (
<Route path="/" component={App}>
<Route path="posts" component={PostList} onEnter={fetchPosts}/>
<Route path="posts/:id" component={PostDetail} />
</Route>
)
}
A better solution is to use something like redial or redux-async-connect. This allows you to co-locate your component's data dependencies with your components, while retaining the ability to test your components without touching the network.
Edit: This applies to an old, no longer supported version of react-router.

I prefer to have actions dispatched from the render prop itself:
<Route to="path" render={ props => {
this.props.toggleInfoLayer(true);
return <UserInfo />;
}} />
This is assuming you are using Redux's mapDispatchToProps argument.
I tried using the history change event handler as mentioned in the accepted answer, but I found it undesirable to be dispatching actions from a rogue file. One more place I had to think about, when Redux already provides plenty too many.

Related

How to use window.alert() when redirected to a page that doesn't exist in react?

<Route path="*">
<Redirect to="/" />
</Route>
The code above is what I am using to handle all not found routes. I have all routes in switch and for the most part this is working fine <Switch>. I was wondering how I would go about sending a windows alert saying something along the lines of "this page doesn't exist" when the unknown route is called. I tried using render and calling a function, but that didn't seem to work. Any thoughts?
edit: to make it clear, I am using React framework, javascript, react-router.
Your current implementation simply redirects back to (/) when the route is not found.
However, to display a window.alert() you should consider making a new NotFound component and handle the display of the alert on that page.
Afterward, use the useHistory hook to redirect back to '/'
See example code below
NotFound Component
import { useEffect } from "react";
import { useHistory } from "react-router-dom";
const NotFound = () => {
let history = useHistory();
useEffect(() => {
window.alert('Page Not found');
history.push('/');
}, []);
return (
<div>404</div>
)
}
export default NotFound;
Existing Routes
<Route path="*">
<NotFound />
</Route>

Passing props to dynamically rendered components in React

I have a page that is displaying several of my star components that each have their own name and a prop called starType
I am generating several different of these stars with the following code
if (num > 0) {
return (
<div className="starWrapper">
<Star
name={`${makeid()}`}
starType={`${starList[Math.floor(Math.random() * 6 + 1)]} ${posList[Math.floor(Math.random() * 9 + 1)]}`}
></Star>
{makeStars((num - 1))}
</div>
);
And this is the star component
<NavLink to={props.name}>
<h1 className="star-label">{props.name}</h1>
<div className={``}>
<div className={`starBall ${props.starType}`}></div>
</div>
</NavLink>
At the moment I want the user to be able to click on each star and have it lead to a page. I have achieved that with react-router's dynamic routing
<Route
exact
path="/:id"
render={(props) => <GenerateSystem {...props} />}
/>
the issue is I want the page that is generated from my generateSystem component to have the starType prop passed to it by the star component. I am aware of React's one way data flow and I think that might be the issue. How can I pass prop data from an auto generated component to another auto generated component?
My full code is viewable here. The components I'm talking about are in the interstellar-view and systems folder.
since you are passing name through URL params so passing starType using query params is an easy option.
So URL would look like this www.example.com/123?starType=red-giant
In your star.jsx, make a modification like this
<NavLink to={`/${props.name}?starType=${props.starType}`}>
...
</NavLink>
In your App.js, make a modification like this
<Switch >
<Route exact path="/:id" component={GenerateSystem} />
<Route exact path="/sol" component={SolSystem} />
<Route exact path="/" component={Interstellar} />
</Switch>
(We do not need to render and pass props since we can use useParams in GenerateSystem.js)
In your GenerateSystem.js, make a modification like this
import React from "react";
import { Link, useLocation, useParams } from "react-router-dom";
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export const GenerateSystem = (props) => {
const {name} = useParams();
const query = useQuery();
const starType = query.get('starType')
return(<div className={starType}>System <p>{name}</p></div>)
}
Refs:
https://reactrouter.com/web/api/Hooks/useparams
https://reactrouter.com/web/example/query-parameters
EDIT:
You can use Redux-store/Context-API to have a global store, so that name and starType can be stored globally and can be accessed in different components
More Use-cases Example -> for other people that came here:
As in React-Router-Dom V6-> there is no render method any more,
See Why does have an element prop instead of render or component?
We mentioned this in the migration guide from v5 to v6, but it's worth repeating here.
In React Router v6 we switched from using v5's and APIs to . Why is that?...
So I needed another way of dynamically rendering all routes for the Router, with a pre declared array with all routes:
const routingList = [{title: 'Home', search: '/', component: Home, icon: 'fa-home'},{...}]
<Routes>
{
routingList.map((routing) => {
let Child = routing.component;
return <Route key={routing.search} path={routing.search} element={<Child {...routing.compProps} />} />;
})
}
<Route path="*" element={<Notfound />} />
</Routes>
(BTW: if you also need the useLocation or the other hooks, and you are using React Class and not React functions, see my answer here:
Component with router props - For: Hooks can only be called inside of the body of a function component
)

How can I avoid unnecessary re rendering of an HOC component, when the parameter component renders in ReactJS with react router

I am using HOC(Layout component here) to wrap my custom component. The Layout component contains Header and sidebar. On clicking link, it will be rendering the respective component. But my problem is that with every route, my HOC gets rendered as route target component is wrapped in this HOC. How can I make my HOC render only once.
Example Snippet.
App.js
<Router>
<Switch>
<PrivateRoute path="routeOne" component={RouteOne}/>
<PrivateRoute path="routeTwo" component={RouteTwo}/>
</Switch>
</Router>
RouteOne.js
import React from "react"
import Layout from "/hoc"
const RouteOne = () =>{
return({..jsx..})
}
export default Layout(RouteOne)
Layout.js
const Layout(WrappedComponent) => {
const userDetails = useSelector(state);
useEffect(()=>{
dispatch(fetchSomething())
},[dispatch])
return ( <HeaderNavbarUILayout header={<Header
username={userDetails.userName}>
content={<WrappedComponent/>);
}
export default Layout
I want to render my HOC component only once. How can I do that?
EDIT:
I would follow this pattern https://simonsmith.io/reusing-layouts-in-react-router-4
const DefaultLayout = ({component: Component, ...rest}) => {
return (
<Route {...rest} render={matchProps => (
<div className="DefaultLayout">
<div className="Header">Header</div>
<Component {...matchProps} />
<div className="Footer">Footer</div>
</div>
)} />
)
};
then where you would normally define routes, replace it with this:
<DefaultLayout path="/" component={SomeComponent} />
I would take a look at the following docs:
how to use useEffect
https://reactjs.org/docs/hooks-reference.html#useeffect
how to implement should component update
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-shouldcomponentupdate
conditionally firing an effect
```
useEffect(
() => {
...
};
},
[whatever you're watching],
);
```
The hoc being wrapped around is rendered every time and that is expected. But the React's diffing algorithm will render only the changed DOM elements. The problem here was the dispatch is being called every time when the Layout page is rendered and the state gets updated and the particular DOM hence gets updated. This gives an impression of "reload" effect. Dispatching the action conditionally will do the trick. Dispatch only when the state changes.

Google Analytics setup for React

I have managed to set up Google Analytics for my React application with the ReactGA library so it will send the pageview to analytics when the user navigates around.
The problem
The problem I'm facing is that I'm not sending any analytics to google on the initial page load, since the history.listen method only fires when the location changes.
My setup
In the root of my project, I initialize the connection:
const history = require("history").createBrowserHistory;
import { Router } from "react-router-dom"
ReactGA.initialize(envConstants.ANALYTICS_TRACKING_ID);
const MyApp = () => (
<Router history={history}>
<MyRoutes />
</Router>
)
Since I only want to see which routes the users are on I have this in my router:
const MyRoutes = props => {
props.history.listen(location => {
// won't see this on initial load, but navigating to another route will give me this
console.log("LISTENING")
})
return (...)
}
So I wonder how I can come around this and send the first/ initial pageview when a user comes to my site. I believe I cannot achieve this with the history.listen method. So, I guess we have to add some other functionality that I'm not too sure of.
I appreciate all the help I can get with this. And if there's something unclear, please let me know.
Thanks for reading and have a nice day!
The issue is that history listen is not called on initial page load since it's only called when the location changes. Try something like the following
import { Router } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import ReactGA from 'react-ga';
const trackPageView = location => {
ReactGA.set({ page: location.pathname });
ReactGA.pageview(location.pathname);
};
const initGa = history => {
ReactGA.initialize('UA-XXXXXX-X', {
debug: true
});
trackPageView(history.location);
history.listen(trackPageView);
};
const history = createHistory();
initGa(history);
ReactDOM.render((
<Router history={history}>
<Layout />
</Router>
), document.getElementById('root'));

How do I reload a page with react-router?

I can see in this file (https://github.com/ReactTraining/react-router/blob/v0.13.3/modules/createRouter.js) that there is a refresh function but I have no idea how to call it. I'm fairly new to react-router, I've only used it to move between some pages a couple times using hashHistory.
Right now I am trying to use it so that when an install fails, the user is given the option to 'retry' which I plan to execute by refreshing the page where the install happens (the page the user would be currently on). Any help would be appreciated.
This is a node app that runs on electron, not a web app.
firstly, add react-router as a dependency
yarn add react-router or npm install react-router
Then (for react-router v5)
import { useHistory } from 'react-router'
const history = useHistory()
// then add this to the function that is called for re-rendering
history.go(0)
This causes your page to re-render automatically
For react-router v6 use the useNavigate hook instead:
import { useNavigate } from 'react-router'
const navigate = useNavigate()
// refresh
navigate(0)
If you're using react-router v6
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
const refreshPage = () => {
navigate(0);
}
You can use this to refresh Current route:
import createHistory from 'history/createBrowserHistory'
const history = createHistory();
history.go(0)
You don't really need react-router for this. You can just use location.reload:
location.reload();
Also that version of react-router you linked to is very old, I think it's linking to v1 when it's currently on v4.
I guess that you're using react-router.
I'll copy my answer from another post.
So you have few possibilities to do that, currently my favorite way to do that is using anonymous function in component prop:
<Switch>
<Route exact path="/" component={()=><HomeContainer/>} />
<Route exact path="/file/:itemPath/:refHash" component={()=><File/>} />
<Route exact path="/:folderName" component ={()=><Folder/>}/>
</Switch>
Or if you want to refresh with current url params, you'll need extra route (reload), and play a little with router stack:
reload = ()=>{
const current = props.location.pathname;
this.props.history.replace(`/reload`);
setTimeout(() => {
this.props.history.replace(current);
});
}
<Switch>
<Route path="/reload" component={null} key="reload" />
<Route exact path="/" component={HomeContainer} />
<Route exact path="/file/:itemPath/:refHash" component={File} />
<Route exact path="/:folderName" component ={Folder}/>
</Switch>
<div onClick={this.reload}>Reload</div>
React
window.location.reload();
working
if you want to re-fetch the data just do the below:
import { useLocation } from 'react-router'
const location = useLocation()
useEffect(() => {
fetchData()
}, [location.key])
I know that this is old, but I found a simple solution according to the documentation of react-router.
Just put that attribute on your Router, and whenever you are on a new Path it will force the page to reload itself.
<Router forceRefresh={true}>
Source:
https://reactrouter.com/web/api/BrowserRouter/forcerefresh-bool
This solution won't cause the undesired full page reload but requires you to make this modification to each page that needs refreshing:
export const Page = () => {
const location = useLocation();
return <PageImpl key={location.key} />
}
So the idea is: create a wrapper around your page and make React re-create the actual page every time the location key changes.
Now it's enough to call history.push(/this-page-route) again and the page refreshes.
If you want to use <Link/> to reload some route, or simply have single history push, you can setup <Redirect/> route under <Switch/> like this:
<Switch>
<Route exact path="/some-route" component={SomeRoute} />
<Redirect exact from="/some-route/reload" to="/some-route" />
</Switch>
And then <Link to="/some-route/reload" /> or push("/some-route/reload")
If you don't want to reload all scripts again you can replace the current path with a fake/empty path and replace it again with the current path like this
// ...
let currentPath = window.location.pathname;
history.replace('/your-empty-route');
setTimeout(() => {
history.replace(currentPath)
}, 0)
// ...
Update:
If the changing of the address bar bothering, you can add a patterned route like this:
<Route path="/*/reload" component={null}/>
and add /replace to the end of currentPath to replace the router with null component. like this:
// ...
let currentPath = window.location.pathname;
history.replace(`${currentPath}/replace`);
setTimeout(() => {
history.replace(currentPath)
}, 0)
// ...
In this way, the reload keyword will add to the end of your current path and I think it's more user friendly.
Notice: If you already have a route that ends with replace It will cause conflict. To solve that you should change the path of the patterned route to something else.
You could try this workaround:
// I just wanted to reload a /messages page
history.pushState(null, '/');
history.pushState(null, '/messages');
You can use this function.
function reloadPage(){
window.location.reload();
}
<input type="button" onClick={ reloadPage } value="reload"/>
May be you are trying to push in history object, then bind your component with withrouter or use window.location.href = url to redirect ..
With React Router 6 you can simply write :
import {useNavigate} from "react-router-dom";
const navigate = useNavigate()
const goToPageOnClick = () =>{
navigate(target_url)
navigate(0)
}
You can achieve that with React Router v6.
import React from 'react';
import { useNavigation, useLocation } from 'react-router-dom';
const Component = () => {
const history = useNavigation();
const location = useLocation();
const reload = () => {
navigate(location.pathname);
};
return (
...
);
};
and then put your reload function inside a useEffect hook.
PS: but this is a weird question, since react-router reloads the page automatically.
If you are needing an asynchronous reload, use history.go(0) (it wraps the History.go() method).
If you need to reload the page synchronously, use history.push(location.pathname) (it wraps the History.pushState() method).
Since there are already examples here using history.go(0), here's an example using history.push(location.pathname):
import React from 'react';
import { useHistory, useLocation } from 'react-router-dom';
const Component = () => {
const history = useHistory();
const location = useLocation();
const reload = () => {
history.push(location.pathname);
};
return (
...
);
};
update webpacker.yml
devServer: {
historyApiFallback: true,
}
Well, the easiest way is to first identify a route for reload and thereafter call the window.location.reload() function on the route like so:
<Switch>
<Route exact exact path="/" component={SomeComponent} />
<Route path="/reload" render= {(props)=>window.location.reload()} />
</Switch>
I recently had the same problem and created this(https://github.com/skt-t1-byungi/react-router-refreshable).
<Refreshable>
<Switch>
<Route path="/home">
<HomePage />
</Route>
<Route path="/post">
<PostPage />
</Route>
{/* ... */}
</Switch>
</Refreshable>

Categories