How to use (and test) React Router with screen resize
When I started to study and learn React, I met React Router and how this library can help us inside a web application.
I was developing a web app, thinking in mobile first, where during the screen resize, an action button should be changed together with the URL link.
Before to include this in my application, I created a React App to validate my idea, to test only this behavior. Let’s check it out.
Creating a new React App
The first step is to create a new React App to play. I highly recommend when you’re developing something and find some particular challenge where you haven’t sure how to do, to start a separated react app and to try different experiments until finding a good solution for your project.
It’s pretty easy to validate and test a solution in a separate way.
To create a new React App, follow the steps on official documentation.
npx create-react-app test-router-sreen
cd test-router-sreen
npm start / yarn start
Let’s start creating a simple JSX, in our “app” will be organized with only three components: Device / Mobile / App, this last one is our Index.
In my test app, I used Material UI (get started here), because it’s a library that’s I love to work, but you can use the only HTML and insert CSS if you want.
The app test
The idea of this app is to have a screen with a button ‘Show Mobile’, and when the user does the action ‘click’ on this button, a component is rendered with a message: ‘ I am rendered only on mobile’, and when moving the screen size, the button change for ‘Show Device’, then click on the button, shows a message ‘I am rendered only on device’.
//Appimport React, { Component } from 'react'
import Button from '@material-ui/core/Button'
import Hidden from '@material-ui/core/Hidden'class App extends Component {
render() {
return (
<div className="App">
<Hidden xsDown>
<Button variant="contained" color="primary">
Show Device
</Button>
</Hidden>
<Hidden smUp>
<Button variant="contained" color="secondary">
Show Mobile
</Button>
</Hidden>
</div>
);
}
}export default App;
I applied the Hidden of Material UI for the button show / hidden at the breakpoints.
The components Device and Mobile was created very simple.
//Device//import React, { Component } from 'react';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';class Device extends Component {
render() {
return (
<div>
<Typography component="h2" variant="h1" gutterBottom>
I am rendered only on Device :)
</Typography>
</div>
);
}
}export default Device;//Mobile//import React, { Component } from 'react';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';class Mobile extends Component {
render() {
return (
<div>
<Typography component="h2" variant="h1" gutterBottom>
I am rendered only on Mobile :)
</Typography>
</div>
);
}
}export default Mobile;
Starting React Router
The challenge now is:
- Tho show the component Mobile when to click on ‘Mobile’ button, or to show the component Device when clicking on button ‘Device,’ changing the URL to /mobile ou /device.
- Moving the screen size with the component rendered, the route needs to change between /mobile’ to index ‘/’ or /device’ to index ‘/’, automatically.
To use React Router in our project, we use the instructions on the documentation to install the package.
npm install react-router-domoryarn add react-router-dom
First of all, let’s create the route inside the buttons Mobile and Device. We can’t forget to import the dependencies.
//App...
import
{ BrowserRouter as Router,
Route,
Link } from "react-router-dom"
import Mobile from './Mobile'
import Device from './Device'
And include the Route at the code with Link on the buttons.
class App extends Component {
render() {
return (
<Router>
<div className="App">
<Hidden xsDown>
<Link to='/mobile'>
<Button variant="contained" color="primary">
Show Device
</Button>
</Link>
</Hidden>
<Hidden smUp>
<Link to='/mobile'>
<Button variant="contained" color="secondary">
Show Mobile
</Button>
</Link>
</Hidden>
<Route path="/device" component={Device} />
<Route path="/mobile" component={Mobile} />
</div>
</Router>
);
}
}export default App;
Alright, the route for components is linked on the button, when we click on render the component and change the path, but an important point is missing now. During the screen resize, change the route but isn’t show the actual component.
To see the resize event on the screen, and dispatch an action, e use a package React Resize Detector.
Installing…
npm i react-resize-detector
// or
yarn add react-resize-detector
On the documentation of this package, is possible to see that we can apply using different forms, as Callback Pattern, Child Function Pattern, Child Component Pattern…etc. Is Child Component Pattern to know the exact point where the resize should apply the Redirect and change the URL route.
Adding the Redirect on imports of react-router-dom and React Resize Detector.
//App...
import
{ BrowserRouter as Router,
Route,
Link,
Redirect } from "react-router-dom"
import ReactResizeDetector from 'react-resize-detector'
...
First, we include the<ReactResizeDetector />
inside the DOM tree, and we verify the breakpoint of Material UI for mobile, where the button change from Device to Mobile is smaller than width 600 px.
Using this information, add a constant, with a rule of resizing, using ternary operator and Redirect do Router to change the route according to with of the screen size. We created a function CheckIfRedirect
.
const CheckIfRedirect = ({width, height}) => {
const path = window.location.pathname
console.log('resize', width, height, path)if ((width < 600 && path === "/device") || (width >= 600 && path === "/mobile")) {
return <Redirect to={{pathname: "/"}} />
} else {
return <></>
}
}
With this rule, it’s specific when the width is smaller than 600 px and the path ‘/device’ or with bigger or equal 600 px and the path ‘/mobile’ then to redirect to pathname: ‘/’ in this case, is our Index without a specific component.
The information window.location.pathname
is possible to find inspecting the console of the browser.
So, separating the code inside the function App in another function, we called Body.
const Body = () => (
<div className="App">
<Hidden xsDown>
<Link to='/device'>
<Button variant="contained" color="primary">
Show Device
</Button>
</Link>
</Hidden>
<Hidden smUp>
<Link to='/mobile'>
<Button variant="contained" color="secondary">
Show Mobile
</Button>
</Link>
</Hidden>
<Route path="/device" component={H1Device} />
<Route path="/mobile" component={H1Mobile} />
</div>
)
To organize our code, inside the App we will include the Router only on Body, theReactResizeDetector
and this new function of CheckIfRedirect
.
class App extends Component {
render() {
return (
<Router>
<>
<Body />
<ReactResizeDetector handleWidth handleHeight>
<CheckIfRedirect />
</ReactResizeDetector>
</>
</Router>
);
}
}
In the end, the complete code of our application:
import React, { Component } from 'react'
import Button from '@material-ui/core/Button'
import Hidden from '@material-ui/core/Hidden'
import { BrowserRouter as Router, Route, Link, Redirect } from "react-router-dom"
import Mobile from './Mobile'
import Device from './Device'
import ReactResizeDetector from 'react-resize-detector'const Body = () => (
<div className="App">
<Hidden xsDown>
<Link to='/device'>
<Button variant="contained" color="primary">
Show Device
</Button>
</Link>
</Hidden>
<Hidden smUp>
<Link to='/mobile'>
<Button variant="contained" color="secondary">
Show Mobile
</Button>
</Link>
</Hidden>
<Route path="/device" component={H1Device} />
<Route path="/mobile" component={H1Mobile} />
</div>
)const CheckIfRedirect = ({width, height}) => {
const path = window.location.pathnameconsole.log('resize', width, height, path)if ((width < 600 && path === "/device") || (width >= 600 && path === "/mobile")) {
return <Redirect to={{pathname: "/"}} />
} else {
return <></>
}
}class App extends Component {
render() {
return (
<Router>
<>
<Body />
<ReactResizeDetector handleWidth handleHeight>
<CheckIfRedirect />
</ReactResizeDetector>
</>
</Router>
);
}
}export default App;
Final result
In the image below, it is possible to see the button changing from Device to Mobile
And clicking on the button, the change of the path.
Testing our application
To write a test is essential, to certify that things are working as expected.
I found information about how to test this application on the documentation of React-Testing-Examples. Another library that helps me a lot was the DOM Testing Library.
Instaling…
npm install react-testing-libraryoryarn add react-testing-library
To check that’s everything it’s going well, let’s start to create a test to render the main component.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { render, waitForElement } from 'react-testing-library'const renderComponent = () => (
render(
<App />
)
)test('renders without crashing', async () => {
const { getByText } = renderComponent();await waitForElement(() => getByText('Show Device'));
});
Now, testing the event to click on the button and render the message that the browser is on device size.
To simulate the click, we add the fireEvent
, passing the message that should be rendered as a string.
...
import { render, waitForElement, fireEvent } from 'react-testing-library'...test('click on show device', async () => {
const { getByText } = renderComponent();fireEvent.click(getByText('Show Device'));
await waitForElement(() => getByText('I am rendered only on Device:)'));});
Now start a delicate part of our test, to simulate the screen resize, changing the button for mobile.
For this, we include a function, using global.innerWidth
as a standard size for a device (1024 px), and adding global.dispachEvent
to dispatch a new 'resize' event.
The test to render the component and the click simulation is the same as before, for device, including only a new size for the screen.
...afterEach(() => {
global.innerWidth = 1024
global.dispatchEvent(new Event('resize'))
});test('click on show device', async () => {
const { getByText } = renderComponent();fireEvent.click(getByText('Show Device'));
await waitForElement(() => getByText('I am rendered only on Device:)'));
});test('click on show mobile', async () => {
// Change the viewport to 500px.
global.innerWidth = 500;// Trigger the window resize event.
global.dispatchEvent(new Event('resize'));const { getByText } = renderComponent();
fireEvent.click(getByText('Show Mobile'));
await waitForElement(() => getByText('I am rendered only on mobile ;)'));
});test('resize to mobile', async () => {
const { getByText } = renderComponent();
// Change the viewport to 500px.
global.innerWidth = 500;// Trigger the window resize event.
global.dispatchEvent(new Event('resize'));// wait react component render after event dispatch
await waitForElement(() => getByText('Show Mobile'));
fireEvent.click(getByText('Show Mobile'));
await waitForElement(() => getByText('I am rendered only on mobile ;)'));
});
In the end…
This is one solution to use React Router and change the path according to the screen resize, useful in an app where is necessary to change the route and component rendered, changing from mobile to device (or vice-versa)
I know that exist other ways to get the same final result using different tools. And you, how to develop your code to create something like this? For me, it is nice to receive different opinions and technical experiences to share knowledge. :)
You can read this post in Portuguese here.