React lazy/Suspens + React Router dont change route until component is fetched - reactjs

I am developing an application that uses the default React code spltting using the Lazy/Suspense approach and the React Router for component rendering. Currently, when I navigate to another path, if the network speed is slow, the path is updated and the fallback component is rendered while the component is fetched, is there any way to wait on the current path until the component package is completely downloaded?

Yes, in concurrent mode, where useTransition() is enabled, you can create a custom router to wrap each of the navigation methods on your history object in a suspense transition:
import { useState, unstable_useTransition as useTransition } from 'react';
import { Router } from 'react-router-dom';
const SuspenseRouter = ({ children, history, ...config }) => {
const [startTransition, isPending] = useTransition(config);
const [suspenseHistory] = useState(() => {
const { push, replace, go } = history;
history.push = (...args) => {
startTransition(() => { push.apply(history, args); });
};
history.replace = (...args) => {
startTransition(() => { replace.apply(history, args); });
};
history.go = (...args) => {
startTransition(() => { go.apply(history, args); });
};
});
suspenseHistory.isPending = isPending;
return (
<Router history={suspenseHistory}>
{children}
</Router>
);
};
export default SuspenseRouter;
Example usage might look something like this:
import { Suspense, lazy, unstable_createRoot as createRoot } from 'react';
import { Switch, Route } from 'react-router-dom';
import { createBrowserHistory } from 'history';
import SuspenseRouter from './components/SuspenseRouter';
const history = createBrowserHistory();
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<SuspenseRouter history={history} timeoutMs={2000}>
<Suspense fallback="Loading...">
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</SuspenseRouter>
);
createRoot(document.getElementById('root')).render(<App />);
Set timeoutMs to Infinity if you want to wait indefinitely on the previous route. In the example above, setting it to 2000 should wait on the previous route for up to 2 seconds, then display the fallback if the code for the requested route hasn't downloaded by then.

Here is another option: instead of suspending url change you can suspend screen change.
Package react-router-loading allows to show loading bar and fetch some data before switching the screen.
Just use Switch and Route from this package instead of react-router-dom:
import { Switch, Route } from "react-router-loading";
Add loading props to the Route where you want to wait something:
<Route path="/my-component" component={MyComponent} loading/>
And then somewhere at the end of fetch logic in MyComponent add loadingContext.done();:
import { LoadingContext } from "react-router-loading";
const loadingContext = useContext(LoadingContext);
const loading = async () => {
//fetching some data
//call method to indicate that fetching is done and we are ready to switch
loadingContext.done();
};

Related

React router v6 programmatically redirect

I am in /customerOrders/13 page and from there I try to redirect to /customerOrders/14 using navigate('/customerOrders/14'). Even though the URL is updated, page is not redirected to /customerOrders/14.
Below are code fragments I extracted related to this from the codebase.
App.js
import {
BrowserRouter,
Routes,
Route
} from "react-router-dom";
...
<BrowserRouter>
<Routes>
<Route path="customerOrders/:id" element={<CustomerOrderForm />}></Route>
</Routes>
<Router>
CustomerOrderForm.jsx
import { useNavigate } from "react-router-dom";
...
const CustomerOrderForm = () => {
let navigate = useNavigate();
const save = async () => {
//
// logic to persist data goes here...
//
navigate(`/customerOrders/${customerOrderId}`);
}
...
}
When you are on a given route:
<Route path="customerOrders/:id" element={<CustomerOrderForm />} />
and navigating to the same route already rendering the mounted component then the component needs to "listen" for changes to the route, in this case, specifically the id route match param that is updated. Use an useEffect hook with a dependency on the id route match param to rerun any logic depending on it.
import { useNavigate, useParams } from "react-router-dom";
...
const CustomerOrderForm = () => {
const navigate = useNavigate();
const { id } = useParams();
useEffect(() => {
// rerun logic depending on id value
}, [id]);
const save = async () => {
//
// logic to persist data goes here...
//
navigate(`/customerOrders/${customerOrderId}`);
};
...
};

react-router-dom useHistory() not working

The useHistory() hook is not working in my project. I have it in different components but none of them work.
I am using "react-router-dom": "^5.2.0",
import {useHistory} from 'react-router-dom'
const history = useHistory()
const logout = () => {
toggleMenu()
setUser(null)
dispatch({type: 'LOGOUT'})
history.push('/')
}
And also in action its not working
export const signin = (formdata, history, setError) => async (dispatch) => {
try {
const {data} = await API.signIn(formdata)
if(data.error){
setError(data.message)
}else{
dispatch({type: SIGNIN, data})
history.push('/dashboard')
}
} catch (error) {
setError(error)
console.log(error)
}
}
Here is my router.js file
const Router = () => {
const user = JSON.parse(localStorage.getItem('profile'))
return (
<BrowserRouter>
<>
{user && <Navigation/>}
<Switch>
<Route path='/page-not-found' component={Auth}/>
<Route path='/' exact component={()=>((user && user.result) ? <Redirect to="/dasboard"/> : <Auth/>)} />
<Route path='/dashboard' component={() => (user ? <Dashboard/> : <Redirect to='/'/>)} />
<Route path='/books-purchase' component={() => (user ? <BooksPage/> : <Redirect to='/' />)} />
</Switch>
</>
</BrowserRouter>
)
}
For the version of react router dom less than 6.^
You can use useHistory() hook like your code is showing
For the latest version of react router dom greater than 6.^
You can use useNavigate() hook like this. You can also use without props
import { useNavigate } from 'react-router-dom';
class Login extends Component {
....
let navigate = useNavigate();
navigate('/');
.....
Instead of useHistory, try useNavigate:
import { useNavigate } from 'react-router-dom';
After some investigation, I found that there is a bug in react-router-dom version ^5.2.0. See this and this . I would suggest you to downgrade react-router-dom version to 4.10.1
In my point of view, It doesn't need to downgrade react-router-dom, just use "useNavigate" hook instead of "useHistory" hook. For example if you also want to send some data to your dashboard page, you can use it like so:
import { useNavigate } from 'react-router-dom'
const FunctionalComponent = () => {
const navigate = useNavigate();
const TestHandler = (someData) => {
navigate("/dashboard", {state: {someData: someData}});
//and if you don't want to send any data use like so:
//navigate("/dashboard");
}
}
Finally, if you want to use that sent data for example in your dashboard page, you can do it like so:
import { useLocation } from 'react-router-dom'
export const Dashboard = () => {
const location = useLocation();
useEffect(() => {
if (location.state.someData) {
// "someData" is available here.
}
});
You can refer to this migration guide: https://reactrouter.com/docs/en/v6/upgrading/v5
i have same error, I forgot add type to button that working function. when i give type,problem was solved
Sometimes, if we call history in dialog or modal it shows error, pass the method to components
const methodTopass= () => {
history.push("/route");
};
<componet methodTopass/>
left "react-router-dom": "^5.2.0", add "history": "4.10.1", in to package.json
then use import { createBrowserHistory } from "history";
and
let hist = createBrowserHistory();
hist.push("/domoy");

Mock Router in React testing library and jest

I'm writing unit test with React testing library and Jest and wants to check if my React Component is successfully able to navigate to next Page.
import { fireEvent, render, screen } from "#testing-library/react";
import React from 'react';
import { Provider } from 'react-redux';
import '#testing-library/jest-dom';
import appStore from '../../src/app/redux/store';
import { MemoryRouter, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const setup = (initialEntries = []) => {
let inMemHistory = createMemoryHistory({ initialEntries });
const utils = render(
<Router history={inMemHistory}>
<Provider store={appStore}>
<Component-1 />
</Provider>
</Router>
);
const saveButtonElem = screen.getByRole('button', { name: "Save and Continue" });
return {
saveButtonElem,
inMemHistory,
...utils,
}
};
Test:
test('should be able to navigate', async () => {
const {
saveButtonElem,
inMemHistory,
getByText,
queryByText,
queryAllByText,
} = setup(["/component_add"]);
// Content of Test
// Saving Config
fireEvent.click(saveButtonElem);
console.info("Current Path", inMemHistory.location.pathname);
// Got /component_add on console
// Expected /component_data after clicking on save button
})
I've tried waiting for 5 second after clicking save button and then tried to print path, but results are same.
Assuming you use react-router, You can use the Memory router for the testing which is easier and performant. I might have typos or syntax errors as I type without IDE support. But it should help you with idea on what I propose.
Option 1:
it("should route to the expected page", () => {
let mockHistory, mockLocation;
render(
<MemoryRouter initialEntries={["/currentUri"]}>
<Component1 />
// Dummy route that routes for all urls
<Route
path="*"
render={({ history, location }) => {
mockHistory= history;
mockLocation= location;
return null;
}}
/>
</MemoryRouter>
);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(mockLocation.pathname).toBe("/expectedUri");
});
Option 2:
import { createMemoryHistory } from 'history';
import { Router } from 'react-router';
const renderWithHistory = (initialEntries= [], Component) => {
let inMemHistory = createMemoryHistory({
initialEntries
});
return {
...render(
<Router history={inMemHistory}>
<Component />
</Router >
), history };
};
it("should route to the expected page", () => {
const { history } = renderWithHistory(['/currentUri'], Component1);
// navigate here on event
userEvent.click(screen.getByRole('button', {name: /Save/}));
expect(history.location.pathname).toBe("/expectedUri");
});

trying to use cookies in my react app but it gets re-rendered all the time

I have a react app with sign in/out functionality. I want the application to keep logged in after reloading, and for that I'm using cookies.
The problem is that after I implemented what I thought should work, I get a "warning, maximum update depth exceeded. it can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render" message.
What am I doing wrong?
Thanks a lot!!
Here is the code.
sessions.ts
import React from "react";
import * as Cookies from "js-cookie";
export const setSessionCookie = (session: any): void => {
Cookies.remove("session");
Cookies.set("session", session, { expires: 14 });
};
export const getSessionCookie: any = () => {
const sessionCookie = Cookies.get("session");
if (sessionCookie === undefined) {
return {};
} else {
return JSON.parse(sessionCookie);
}
};
export const SessionContext = React.createContext(getSessionCookie());
main.tsx
import React, { useState, useEffect } from "react";
import { Switch, Route, Router } from "react-router-dom";
import Comp1 from "./components";
import Comp2 from "./components";
import { getSessionCookie, SessionContext } from "./session";
import { createBrowserHistory } from "history";
export default function Main() {
const [session, setSession] = useState(getSessionCookie());
useEffect(() => {
setSession(getSessionCookie());
}, [session]);
const history = createBrowserHistory();
return (
<SessionContext.Provider value={session}>
<Router history={history}>
<Switch>
<Route exact path="/" component={Comp1} />
<Route path="/comp2" component={Comp2} />
</Switch>
</Router>
</SessionContext.Provider>
);
}
I think the problem is that inside your useEffect you are changing the session, and then watching for session changes. You could probably compare the new cookie and only update the session if the cookie has changed -
useEffect(() => {
const cookie = getSessionCookie();
if (cookie !== session) setSession(cookie);
}, [session]);
Not sure exactly what your cookie looks like - you may have to do a more complex comparison than !==.
const [session, setSession] = useState(getSessionCookie());
You get session
useEffect(() => { ... }, [session]);
Session changed, effect callback called.
setSession(getSessionCookie());
We are setting a new session here (note that getSessionCookie always returns a new object). Component will re-render.
Back to the top.

Detect Route Change with react-router

I have to implement some business logic depending on browsing history.
What I want to do is something like this:
reactRouter.onUrlChange(url => {
this.history.push(url);
});
Is there any way to receive a callback from react-router when the URL gets updated?
You can make use of history.listen() function when trying to detect the route change. Considering you are using react-router v4, wrap your component with withRouter HOC to get access to the history prop.
history.listen() returns an unlisten function. You'd use this to unregister from listening.
You can configure your routes like
index.js
ReactDOM.render(
<BrowserRouter>
<AppContainer>
<Route exact path="/" Component={...} />
<Route exact path="/Home" Component={...} />
</AppContainer>
</BrowserRouter>,
document.getElementById('root')
);
and then in AppContainer.js
class App extends Component {
componentWillMount() {
this.unlisten = this.props.history.listen((location, action) => {
console.log("on route change");
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<div>{this.props.children}</div>
);
}
}
export default withRouter(App);
From the history docs:
You can listen for changes to the current location using
history.listen:
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
The location object implements a subset of the window.location
interface, including:
**location.pathname** - The path of the URL
**location.search** - The URL query string
**location.hash** - The URL hash fragment
Locations may also have the following properties:
location.state - Some extra state for this location that does not reside in the URL (supported in createBrowserHistory and
createMemoryHistory)
location.key - A unique string representing this location (supported
in createBrowserHistory and createMemoryHistory)
The action is one of PUSH, REPLACE, or POP depending on how the user
got to the current URL.
When you are using react-router v3 you can make use of history.listen() from history package as mentioned above or you can also make use browserHistory.listen()
You can configure and use your routes like
import {browserHistory} from 'react-router';
class App extends React.Component {
componentDidMount() {
this.unlisten = browserHistory.listen( location => {
console.log('route changes');
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<Route path="/" onChange={yourHandler} component={AppContainer}>
<IndexRoute component={StaticContainer} />
<Route path="/a" component={ContainerA} />
<Route path="/b" component={ContainerB} />
</Route>
)
}
}
Update for React Router 5.1+.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function SomeComponent() {
const location = useLocation();
useEffect(() => {
console.log('Location changed');
}, [location]);
...
}
react-router v6
In react-router v6, this can be done by combining the useLocation and useEffect hooks
import { useLocation } from 'react-router-dom';
const MyComponent = () => {
const location = useLocation()
React.useEffect(() => {
// runs on location, i.e. route, change
console.log('handle route change here', location)
}, [location])
...
}
For convenient reuse, you can do this in a custom useLocationChange hook
// runs action(location) on location, i.e. route, change
const useLocationChange = (action) => {
const location = useLocation()
React.useEffect(() => { action(location) }, [location])
}
const MyComponent1 = () => {
useLocationChange((location) => {
console.log('handle route change here', location)
})
...
}
const MyComponent2 = () => {
useLocationChange((location) => {
console.log('and also here', location)
})
...
}
If you also need to see the previous route on change, you can combine with a usePrevious hook
const usePrevious = (value) => {
const ref = React.useRef()
React.useEffect(() => { ref.current = value })
return ref.current
}
const useLocationChange = (action) => {
const location = useLocation()
const prevLocation = usePrevious(location)
React.useEffect(() => {
action(location, prevLocation)
}, [location])
}
const MyComponent1 = () => {
useLocationChange((location, prevLocation) => {
console.log('changed from', prevLocation, 'to', location)
})
...
}
It's important to note that all the above fire on the first client route being mounted, as well as subsequent changes. If that's a problem, use the latter example and check that a prevLocation exists before doing anything.
If you want to listen to the history object globally, you'll have to create it yourself and pass it to the Router. Then you can listen to it with its listen() method:
// Use Router from react-router, not BrowserRouter.
import { Router } from 'react-router';
// Create history object.
import createHistory from 'history/createBrowserHistory';
const history = createHistory();
// Listen to history changes.
// You can unlisten by calling the constant (`unlisten()`).
const unlisten = history.listen((location, action) => {
console.log(action, location.pathname, location.state);
});
// Pass history to Router.
<Router history={history}>
...
</Router>
Even better if you create the history object as a module, so you can easily import it anywhere you may need it (e.g. import history from './history';
This is an old question and I don't quite understand the business need of listening for route changes to push a route change; seems roundabout.
BUT if you ended up here because all you wanted was to update the 'page_path' on a react-router route change for google analytics / global site tag / something similar, here's a hook you can now use. I wrote it based on the accepted answer:
useTracking.js
import { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
export const useTracking = (trackingId) => {
const { listen } = useHistory()
useEffect(() => {
const unlisten = listen((location) => {
// if you pasted the google snippet on your index.html
// you've declared this function in the global
if (!window.gtag) return
window.gtag('config', trackingId, { page_path: location.pathname })
})
// remember, hooks that add listeners
// should have cleanup to remove them
return unlisten
}, [trackingId, listen])
}
You should use this hook once in your app, somewhere near the top but still inside a router. I have it on an App.js that looks like this:
App.js
import * as React from 'react'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import Home from './Home/Home'
import About from './About/About'
// this is the file above
import { useTracking } from './useTracking'
export const App = () => {
useTracking('UA-USE-YOURS-HERE')
return (
<Switch>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
)
}
// I find it handy to have a named export of the App
// and then the default export which wraps it with
// all the providers I need.
// Mostly for testing purposes, but in this case,
// it allows us to use the hook above,
// since you may only use it when inside a Router
export default () => (
<BrowserRouter>
<App />
</BrowserRouter>
)
I came across this question as I was attempting to focus the ChromeVox screen reader to the top of the "screen" after navigating to a new screen in a React single page app. Basically trying to emulate what would happen if this page was loaded by following a link to a new server-rendered web page.
This solution doesn't require any listeners, it uses withRouter() and the componentDidUpdate() lifecycle method to trigger a click to focus ChromeVox on the desired element when navigating to a new url path.
Implementation
I created a "Screen" component which is wrapped around the react-router switch tag which contains all the apps screens.
<Screen>
<Switch>
... add <Route> for each screen here...
</Switch>
</Screen>
Screen.tsx Component
Note: This component uses React + TypeScript
import React from 'react'
import { RouteComponentProps, withRouter } from 'react-router'
class Screen extends React.Component<RouteComponentProps> {
public screen = React.createRef<HTMLDivElement>()
public componentDidUpdate = (prevProps: RouteComponentProps) => {
if (this.props.location.pathname !== prevProps.location.pathname) {
// Hack: setTimeout delays click until end of current
// event loop to ensure new screen has mounted.
window.setTimeout(() => {
this.screen.current!.click()
}, 0)
}
}
public render() {
return <div ref={this.screen}>{this.props.children}</div>
}
}
export default withRouter(Screen)
I had tried using focus() instead of click(), but click causes ChromeVox to stop reading whatever it is currently reading and start again where I tell it to start.
Advanced note: In this solution, the navigation <nav> which inside the Screen component and rendered after the <main> content is visually positioned above the main using css order: -1;. So in pseudo code:
<Screen style={{ display: 'flex' }}>
<main>
<nav style={{ order: -1 }}>
<Screen>
If you have any thoughts, comments, or tips about this solution, please add a comment.
import React from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import Sidebar from './Sidebar';
import Chat from './Chat';
<Router>
<Sidebar />
<Switch>
<Route path="/rooms/:roomId" component={Chat}>
</Route>
</Switch>
</Router>
import { useHistory } from 'react-router-dom';
function SidebarChat(props) {
**const history = useHistory();**
var openChat = function (id) {
**//To navigate**
history.push("/rooms/" + id);
}
}
**//To Detect the navigation change or param change**
import { useParams } from 'react-router-dom';
function Chat(props) {
var { roomId } = useParams();
var roomId = props.match.params.roomId;
useEffect(() => {
//Detect the paramter change
}, [roomId])
useEffect(() => {
//Detect the location/url change
}, [location])
}
Use the useLocation() Hook to detect the URL change and put it in dependency array in useEffect() this trick worked for me
const App = () => {
const location = useLocation();
useEffect(() => {
window.scroll(0,0);
}, [location]);
return (
<React.Fragment>
<Routes>
<Route path={"/"} element={<Template/>} >
<Route index={true} element={<Home/>} />
<Route path={"cart"} element={<Cart/>} />
<Route path={"signin"} element={<Signin/>} />
<Route path={"signup"} element={<Signup/>} />
<Route path={"product/:slug"} element={<Product/>} />
<Route path={"category/:category"} element={<ProductList/>} />
</Route>
</Routes>
</React.Fragment>
);
}
export default App;
You can use the useLocation with componentDidUpdate for getting the route change for class component and useEffect for functional component
In Class component
import { useLocation } from "react-router";
class MainApp extends React.Component {
constructor(props) {
super(props);
}
async componentDidUpdate(prevProps) {
if(this.props.location.pathname !== prevProps.location.pathname)
{
//route has been changed. do something here
}
}
}
function App() {
const location = useLocation()
return <MainApp location={location} />
}
In functional component
function App() {
const location = useLocation()
useEffect(() => {
//route change detected. do something here
}, [location]) //add location in dependency. It detects the location change
return <Routes>
<Route path={"/"} element={<Home/>} >
<Route path={"login"} element={<Login/>} />
</Routes>
}
React Router V5
If you want the pathName as a string ('/' or 'users'), you can use the following:
// React Hooks: React Router DOM
let history = useHistory();
const location = useLocation();
const pathName = location.pathname;

Resources