Como aplicar (e testar) React Router para alternância de rota ao redimensionar uma tela

Rebecca Oliveira
8 min readJan 16, 2019

--

Photo by Ryoji Iwata on Unsplash

Logo que comecei a estudar e me dedicar em React, conheci React Router e como ele pode nos ajudar dentro de uma aplicação web.

Estava desenvolvendo um sistema web, pensando em mobile first, me deparei com uma situação, onde ao dar um resize na tela, um botão de ação deveria ser alterado assim como a rota na URL.

Antes de incluir isto na minha aplicação, criei um React app para validar a ideia, para testar exclusivamente essa mudança de botão e rota. Na sequencia explico o passo a passo.

Criando um novo React App

Um primeiro passo é criar um React app para brincar. Recomendo muito sempre que estiverem desenvolvendo algo e se deparar com um dúvida pontual, criem um app separado para fazer vários experimentos até encontrar uma solução mais cabível ao seu projeto.

É mais fácil validar a solução e testar, de forma isolada.

Para criar um React app basta seguir os passos explicados na documentação oficial.

npx create-react-app test-router-sreen
cd test-router-sreen
npm start / yarn start

Vamos começar criando um simples JSX, nosso “app” terá apenas 3 componentes: Device / Mobile / App sendo este último a nossa Index.

Na minha aplicação teste usei Material UI (get started here), pois é uma biblioteca que eu gosto muito de trabalhar, mas você pode usar somente HTML e incluir algum CSS se assim desejar. :)

O app teste

A ideia era ter uma tela com um botão ‘Show Mobile’, que ao clicar, montasse uma componente com a mensagem: ‘ I am rendered only on mobile’, e conforme fosse aumentando o screen size, o botão alterasse para ‘Show Device’, e ao clicar aparecesse a mensagem ‘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;

Usei o Hidden do Material UI para o botão aparecer / desaparecer nos break points.

Já as componentes Device e Mobile ficaram extremamente simples.

//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;

Iniciando o React Router

O desafio agora ficou em :

  1. Montar a componente Mobile quando clicar no botão ‘Mobile’, ou montar a componente Device quando clicar no botão ‘Device’, alternando o path da url para /mobile’ ou /device’.
  2. Ao movimentar a tela, com a componente renderizada e efetuar o resize, a rota deve alterar entre /mobile’ para index ‘/’ ou /device’ para index ‘/’, automaticamente.

Para usar o React Router no nosso projeto seguimos as instruções da documentação para a instalação do pacote.

npm install react-router-dom ouyarn add react-router-dom

Primeiramente vamos criar a rota nos botões de Mobile e Device. Não podemos esquecer de dar um ‘import’ nas dependências.

//App...
import
{ BrowserRouter as Router,
Route,
Link } from "react-router-dom"
import Mobile from './Mobile'
import Device from './Device'

E incluir o Route no código com Link nos botões.

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;

Certo, a rota para as componentes está linkada no botão, quando clicamos aparece a componente na tela e muda a rota, mas falta um ponto que é o mais importante nesse momento. Ao dar o resize, alterar a rota e não renderizar a componente atual, mas sim outra ao clicar no botão com novo screen size.

Para escutar o evento de resize da tela e disparar uma ação, usamos um pacote chamado React Resize Detector.

Instalando…

npm i react-resize-detectorouyarn add react-resize-detector

Na documentação é possível ver que pode ser aplicado de diversas formas, como Callback Pattern, Child Function Pattern, Child Component Pattern…etc. É com Child Component Pattern que iremos setar o exato ponto onde o resize deverá aplicar o Redirect e mudar a rota da URL.

Adicionamos o Redirect nos imports do react-router-dom e o React Resize Detector.

//App...
import
{ BrowserRouter as Router,
Route,
Link,
Redirect } from "react-router-dom"
import ReactResizeDetector from 'react-resize-detector'
...

Primeiro incluímos o <ReactResizeDetector /> dentro da árvore o DOM do App, e verificamos que o ponto do break point do Material UI para mobile, onde o botão muda de Device para Mobile é menor que width 600 px.

Com base nessa informação, adicionar uma constante, com uma regra de resize utilizando operador ternário e Redirect do Router para mudar a rota de acordo com o tamanho da tela. Criamos a função 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 <></>
}
}

Com essa regra deixamos claro que quando o width for menor que 600px e o path ‘/device’ ou with for maior ou igual que 600px e o path ‘/mobile’, então redireciona para pathname: ‘/’ que no caso é a nossa index sem nenhuma componente renderizada.

A informação de window.location.pathnameencontramos ao inspecionar o console do navegador.

Separamos então todo o código que estava até então dentro da função App e separamos em uma nova função, que daremos o nome de 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>
)

Para o nosso código ficar mais organizados, dentro do App vamos colocar apenas o Router com o Body , o ReactResizeDetector e essa nova função do CheckIfRedirect.

class App extends Component {
render() {
return (
<Router>
<>
<Body />
<ReactResizeDetector handleWidth handleHeight>
<CheckIfRedirect />
</ReactResizeDetector>
</>
</Router>
);
}
}

Ao final, o código da nossa aplicação ficou assim.

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.pathname
console.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;

Resultado final

Abaixo podemos ver o botão alterando de Device para Mobile.

E ao clicar o botão, a mudança de rota.

Testando nossa aplicação

Tão importante quanto escrever um código é testa-lo.

Busquei informações de como escrever um teste para esta aplicação na documentação do React-Testing-Examples. Outra biblioteca que me ajudou muito também foi a DOM Testing Library.

Instalando…

npm install react-testing-libraryouyarn add react-testing-library

Para certificarmos de que está funcionando de acordo com o que queremos, vamos começar escrevendo um teste que renderiza a componente inteira.

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'));
});
No terminal, ao rodar o teste a confirmação.

Agora vamos testar o evento de clicar no botão e renderizar a mensagem de que o navegador está no tamanho para device.

Para simularmos o click, adicionamos o fireEvent, passando a mensagem que deverá ser renderizada como uma 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:)'));
});
Teste continua passando, legal :)

Agora entramos em uma parte um pouco mais delicada para o teste, simular o redimensionamento da tela, alterando o botão para mobile.

Para isso, vamos incluir uma função, utilizando global.innerWidth com tamanho padrão de device (1024 px), e adicionando global.dispachEvent um para um novo evento 'resize'.

Vejam que o teste para renderizar a componente e a simulação do click no botão é igual a anterior, para o device, incluímos apenas um novo tamanho de tela.

...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 ;)'));
});
Todos os teste passaram. Aplicação funcionando como esperado.

Observações finais

Esta é uma forma de utilizar React Router e mudar a rota de acordo com redimensionamento da tela, sendo muito útil em um sistema que precisa alterar a rota e a componente quando passar de mobile para device (ou vice e versa).

Sabemos que existe outras meios de obter o mesmo resultado utilizando diferentes ferramentas. E você, como desenvolveria seu código para obter um resultado semelhante? Gosto de ter mais opiniões e experiências técnicas para compartilharmos conhecimentos. :)

Você pode ler este post em Inglês aqui.

--

--