This is the final project from codeacademy's React Router course. The course is written for React Router v5, and includes a few warnings that it won't work if you install React Router 6. Having refactored the previous tutorial into React Router 6, I carried out this project with React Router 6.
I've listed out the main differences between what I did and the actual tutorial. Please:
- flag any mistakes/anything unclear.
- let me know if you'd like to see, or would like to help me, flesh this out into a full tutorial.
There are some tests using jest and React testing library, which are beyond the scope of the tutorial and I've added as a learning exercse.
Run npm install react-router-dom
.
This installs the most recent version of React Router, whereas the provided command
npm install --save react-router-dom@5.2.0
installs specifically v5.2.0. Note the --save
flag was deprecated in npm v5 (current version at time of writing is v8.18), and isn't necessary.
Import Route
and Routes
from react-router-dom
.
Updated Route syntax: <Route path=":type/*" element={<HomePage />} />
:type
sets βtypeβ as a param so it can be grabbed by useParams.*
enables rendering of child links
to look into
This may have been simplified by React Router 6's relative link syntax. Converseley, I found I had to add the home link twice (once with '/'
only, and once as step 5, as trailing ?
to make parameter optional not available in React Router 6.
At this point, App.js looks like this:
import HomePage from './pages/home';
import SearchPage from './pages/search';
import PetDetailsPage from './pages/detail';
import PetDetailsNotFound from './pages/petDetailsNotFound';
import Navigation from './components/navigation';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
return (
<Router>
<div>
<Navigation />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path=":type/*" element={<HomePage />} />
</Routes>
</div>
</Router>
);
}
export default App;
Note to self: from the docs. AΒ <NavLink>
Β is a special kind ofΒ <Link>
Β that knows whether or not it is "active".
I replaced the style class with ternary operator from the example in the docs.
I ignored this step as didn't have the problem it mentioned.
useHistory()
is deprecated - docs on switching to useNavigate()
.
import React, { useRef } from 'react';
// import useNavigate here.
import { useNavigate } from 'react-router-dom';
const Search = () => {
// get the navigate object here
const navigate = useNavigate();
const searchInputRef = useRef();
const onSearchHandler = (e) => {
e.preventDefault();
const searchQuery = new URLSearchParams({
name: searchInputRef.current.value
}).toString();
// imperatively redirect with navigate
navigate(`/search?${searchQuery}`);
};
return (
<form onSubmit={onSearchHandler} className="search-form">
<input type="text" className="search" ref={searchInputRef} />
<button type="submit" className="search-button">
π
</button>
</form>
);
};
export default Search;
Again, useHistory()
deprecated, so import and use useNavigate()
as per the docs.
I have written these tests with jest and React testing library.
Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks.
From LogRocket post comparing react testing libraries:
Jest is a testing framework created and maintained by Facebook. If you build your React application with Create React App, you can start using Jest with zero config. Just add react-test-renderer
and the @testing-library/react library
to conduct snapshot and DOM testing.
With Jest, you can:
- Conduct snapshot, parallelization, and async method tests
- Mock your functions, including third-party node_module libraries
- Execute myriad assertion methods
- View code coverage report
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
This means jest will look for:
- Files with
.js
suffix in__tests__
folders. - Files with
.test.js
suffix. - Files with
.spec.js
suffix. The.test.js
/.spec.js
files (or the__tests__
folders) can be located at any depth under thesrc
top level folder.
Jest recommends putting the test files (or __tests__
folders) next to the code they are testing so that relative imports appear shorter. For example, if App.test.js
and App.js
are in the same folder, the test only needs to import App from './App' instead of a long relative path. Collocation also helps find tests more quickly in larger projects.
From valentiong.com:
Code coverage makes possible to spot untested paths in our code. It is an important metric for determining the health of a project. This condif (from the linked tutorial) configures jest to display coverage when npm test
is run, and for tests to fail if coverage is 90%.
Either add to package.json
{
"name": "cute-pets-website",
"version": "0.1.0",
"private": true,
"jest": {
"collectCoverageFrom": ["./src/**"],
"coverageThreshold": {
"global": {
"lines": 90
}
}
},
etc }
...or create jest.config.js
:
// jest.config.js
const {defaults} = require('jest-config');
module.exports = {
"jest": {
"collectCoverage": true,
"collectCoverageFrom": ["./src/**"],
"coverageThreshold": {
"global": {
"lines": 90
}
}
}
}
NB: create-react-app
doesn't allow collectCoverage
flag to be amended, so need to delete that line and run npm test -- --coverage
each time.
I also created jest.json.js
to be sure jest knew where everthing was:
{
"rootDir": "../",
"setupFiles": [
"<rootDir>/config/setupTests.js",
"<rootDir>/config/jest.config.js"
]
}
The final jest dir structure now looks like:
.
βββ config
β βββ jest.config.js
β βββ jest.json
β βββ setupTests.js
βββ src
βββ __snapshots__
βββ __tests__
βββ api
βββ assets
βββ components
βββ mocks
βββ pages
βββ practice
Generated using tree: tree -L 2 -d -I 'coverage|node_modules|public'
React testing library (RTL) is written to enable testing of your application as if you were the user interacting with the applicationβs interface, searching directly by the text displayed on screen and without the overhead work of finding the element that contains that text.
It does not require any setup and comes preinstalled with create-react-app
.
I have written 3 tests. These are not exhaustive as the purpose here was for me to get some practice writing tests. First of all, I have pasted in App.test.js
, and next up I'll go through the tests line by line:
// App.test.js
gitimport React from 'react';
import { render, cleanup, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
// import modules to test
import App from '../App';
import Hero from '../components/hero/index';
afterEach(cleanup);
test('App should look like the snapshot', () => {
const { asFragment } = render(<App />);
expect(asFragment(<App />)).toMatchSnapshot();
});
test('hero should render', () => {
const { getByText } = render(<Hero />);
expect(getByText('Find your perfect pet'));
});
test('search on home page works', () => {
render(<App />);
// assign search bar button
const button = screen.getByRole('button', { name: 'π' });
// assign search box
const searchBox = screen.getByRole('textbox', { name: 'search-box' });
// type in search box
userEvent.type(searchBox, 'dogs');
// click button to submit search
userEvent.click(button);
// verify search box is there
expect(screen.getByLabelText('search-box')).toBeInTheDocument();
// verify search worked submission worked and new page has loaded
screen.getByText('Results for dogs');
});
And the output:
afterEach
runs the command after each individual test.
cleanup
is an RTL command to unmount React trees mounted with render. Calling this prevents memory leaks and test isolation.
This test takes a snapshot and compares the rendered DOM. It passes if the snapshot taken in the test matches the snapshot stored in file: in other words it confirms the UI has not changed. There are two scenarios when there is a change:
- unexpected change: something has probably broken, and telling us this is the purpose of the test.
- expected change (eg updated a UI component). jest will prompt you to update the snapshot.
Test setup:
test('App should look like the snapshot', () => {
Render App and assign to asFragment:
const { asFragment } = render(<App />);
compare rendered app to stored snapshot:
expect(asFragment(<App />)).toMatchSnapshot(); });
This test uses getByText to search the DOM for the provided text. It passes if the text is found once, and throws an error either if the text is not found, or is found more than once.
Test setup:
test('hero should render', () => {
Unpacks getByText command and assigns to rendered object.
const { getByText } = render(<Hero />);
Pass expected text into getByText.
expect(getByText('Find your perfect pet')); });
This test simulates a search by isolating the search box, entering some text, clicking the search button, and checking the DOM has changed to the search results page.
Test setup:
test('search on home page works', () => {
Render the app:
render(<App />);
Isolate search bar by role and assign to button:
const button = screen.getByRole('button', { name: 'π' });
Assign search box
const searchBox = screen.getByRole('textbox', { name: 'search-box' });
Enter search term dogs into search box
userEvent.type(searchBox, 'dogs');
Click _button to submit search:
userEvent.click(button);
Verify search box is there
expect(screen.getByLabelText('search-box')).toBeInTheDocument();
Verify search worked submission worked and the results page has loaded
screen.getByText('Results for dogs'); });