Router Router Switch causes unmount on updating route search/query strings - reactjs

In my App.js I have a number of components wrapped in a Switch component from react-router-dom
App.js
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
const Test = Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
});
const Test1 = Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
});
const Test2 = Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
});
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="blue" exact path="/" component={Test} />
<ProtectedRoute bgColour="red" exact path="/1" component={Test1} />
<ProtectedRoute bgColour="green" exact path="/2" component={Test2} />
</Switch>
);
};
export default App;
The ProtectedRoute component renders a Route component from react-router-dom passing in the specified component. It also has a HOC, which in my actual application checks the user is authenticated
ProtectedRoute.js
import React from "react";
import { Route } from "react-router-dom";
const withAuth = (Component) => {
return (props) => {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, bgColour, ...args }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route component={withAuth(component)} {...args} />
</div>
);
};
export default ProtectedRoute;
For each component, I have alerts setup to trigger on mount and unmount of the component. On a click on an element it updates the query string to a random number via history.push, however, this currently triggers an unmount, due to the Switch added in App.js, without the Switch there is no unmount. This is causing an issue in my application as an unmount is not desired behaviour and is causing issues with loading the correct data.
Test.js
import React, { useEffect } from "react";
import { useHistory } from "react-router-dom";
export default function Test() {
const history = useHistory();
useEffect(() => {
alert("MOUNTED BASE");
return () => {
alert("UNMOUNTED BASE");
};
}, []);
return (
<div>
<h1>TEST COMPONENT BASE - BLUE</h1>
<div
onClick={() =>
history.push({
pathname: history.location.pathname,
search: `?query=${Math.random().toFixed(2)}`
})
}
>
UPDATE QUERY STRING
</div>
<div onClick={() => history.push("/1")}>GO TO Next ROUTE</div>
</div>
);
}
I still want the functionality of the Switch but prevent the unmount on history.push, is this possible?
I have a CodeSandbox below to recreate this issue

Issue
Based on only the code you've provided, an issue I see is how every component the ProtectedRoute renders is decorated with the withAuth Higher Order Component. This results in a new component being created every time ProtectedRoute renders.
Solution
You want to only decorate the routed components with HOCs once prior to where they are used.
Example:
const ProtectedRoute = ({ bgColour, ...props }) => {
return (
<div style={{ backgroundColor: bgColour || "transparent" }}>
<Route {...props} />
</div>
);
};
...
import React from "react";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import withAuth from "..path/to/withAuth";
// Decorate components with HOCs once out here
const Test = withAuth(Loadable({
loader: () => import("./Test"),
loading: () => <h1>LOADING....</h1>
}));
const Test1 = withAuth(Loadable({
loader: () => import("./Test1"),
loading: () => <h1>LOADING....</h1>
}));
const Test2 = withAuth(Loadable({
loader: () => import("./Test2"),
loading: () => <h1>LOADING....</h1>
}));
// Render decorated components in App
const App = () => {
return (
<Switch>
<ProtectedRoute bgColour="red" path="/1" component={Test1} />
<ProtectedRoute bgColour="green" path="/2" component={Test2} />
<ProtectedRoute bgColour="blue" path="/" component={Test} />
</Switch>
);
};

Related

React with Enzyme test case

import React from 'react';
import PropTypes from 'prop-types';
import { Route } from 'react-router-dom';
import { SelectModal } from 'ux-components';
const ItemSelectRoute = (props) => {
console.log('1111111', props);
return (
<Route
path="/item-select/:label"
render={(routeProps) => (
<SelectModal
isOpen
label={routeProps.match.params.label}
onCloseClick={() => (routeProps.history.push(props.background.pathname))}
/>
)}
/>
);
}
export default ItemSelectRoute;
SelectModal.js
import React from 'react';
import PropTypes from 'prop-types';
import { Dialog } from 'styleguide-react-components';
import ModalHeader from 'ux-components/src/ModalHeader';
import ModalBody from '../../ModalBody/ModalBody';
const SelectModal = ({
onCloseClick, isOpen, itemSummaries,
}) => {
const itemList = itemSummaries;
return (
<Dialog
appearance="lite"
open={isOpen}
title={<ModalHeader header="Please select" />}
type="modal"
hasCloseButton
clickOffToClose
width={750}
onClose={onCloseClick}
>
<ModalBody items={itemList} />
</Dialog>
);
};
export default SelectModal;
I am writing the test case as for ItemSelectRoute
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const state = {
settings: {
configuration: {},
featureToggle: {},
properties: {},
},
};
const store = mockStore(state);
const newProps = {
appData: {
background: {
pathname: '/',
},
};
const wrapper = mount(<ReduxProvider store={store}>
<MemoryRouter initialEntries={['/item-select/test']}>
<Switch>
<ItemSelectRoute
store={store}
dispatch={jest.fn()}
{...newProps}
render={() => (<SelectModal
isOpen
label="track-my-item"
onCloseClick={() => jest.fn()}
/>)}
/>
</Switch>
</MemoryRouter>
</ReduxProvider>);
console.log(wrapper.debug());
When I run the test, I am getting the following error
Cannot read property 'addEventListener' of undefined
I want to write the test case, where if the route is correct, then SelectModal should be present in the elements tree. I tried few options, but I am unable to resolve the error.

How to link to a component using persistant header that's outside of BrowserRouter?

I'm attempting to link to somewhere within my application using react-router-dom within an appBar/header that is persistent throughout the app. I keep getting "TypeError: history is undefined" when I attempt to use RRD within the header component.
I've been playing around with this for a good few hours now and I'm not getting any where with it. I can't think straight thanks to the heat, and I'm clearly searching for the wrong answers in my searches. The best solution I have come-up with thus-far is having each component contain the header component at the top but this is obv not ideal. I know I must be missing something simple as this can't be an uncommon pattern.
Demo Code
Node Stuff
npx create-react-app rdr-header --template typescript
npm install react-router-dom
App.tsx
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
useHistory,
} from "react-router-dom";
function App() {
let history = useHistory();
const handleClick = (to: string) => {
history.push(to);
};
return (
<div className='App'>
<header className='App-header'>
<button onClick={() => handleClick("/ger")}>German</button>
<button onClick={() => handleClick("/")}>English</button>
</header>
<Router>
<Switch>
<Route exact path='/' component={English} />
<Route path='/ger' component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push("/ger");
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push("/");
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
You should create separate component for header
header.js
import React from 'react';
import './style.css';
import { useHistory } from 'react-router-dom';
function Header() {
let history = useHistory();
const handleClick = to => {
history.push(to);
};
return (
<header className="App-header">
<button onClick={() => handleClick('/ger')}>German</button>
<button onClick={() => handleClick('/')}>English</button>
</header>
);
}
export default Header;
Use Header component inside Router like below:-
import React from 'react';
import './style.css';
import {
BrowserRouter as Router,
Switch,
Route,
useHistory
} from 'react-router-dom';
import Header from './header.js'; // import header component
function App() {
return (
<div className="App">
<Router>
<Header /> // use Header component inside Router
<Switch>
<Route exact path="/" component={English} />
<Route path="/ger" component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push('/ger');
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push('/');
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
Instead of changing the history object using history.push(), you can use the <Link> or <NavLink> components from react-router.
React Router - Link component
Make sure to place the header component inside the Router component.

Await for react-router history.goBack to complete

I'm in a route that indexes an array in state. When clicking a button, I want to delete that item out of state. Before doing this, I want to go back to another route that doesn't use the item. I do this to avoid a TypeError when indexing the item that no longer exists.
Is it possible to wait for the route change to complete before updating state? It seems like there is no promise capability with useHistory.
Minimal example:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom'
class App extends Component {
constructor (props) {
super(props)
this.state = {
stuff: [{
message: 'hello'
}]
}
}
componentDidMount () {
console.log('mounted')
}
render () {
return (
<Switch>
<Route path='/1'>
<Child
stuff={this.state.stuff} callback={() => {
this.props.history.goBack()
this.setState({
stuff: []
})
}}
/>
</Route>
<Route path='/'>
<button onClick={() => this.props.history.push('/1')}>
Go to friend
</button>
home
</Route>
</Switch>
)
}
}
const Child = ({ callback, stuff }) =>
<>
<button onClick={callback}>
Go back
</button>
{stuff[0].message} friend
</>
const withUseHistory = (Component) => {
return function WrappedComponent (props) {
const history = useHistory()
return <Component {...props} history={history} />
}
}
const AppWithHistory = withUseHistory(App)
const wrapper = document.getElementById('root')
ReactDOM.render(
<BrowserRouter>
<AppWithHistory />
</BrowserRouter>,
wrapper)
I've found a solution through using history.push instead of history.goBack:
this.props.history.push('/')
this.setState({
stuff: []
})
It seems history.push is synchronous while goBack is async.
history.goBack();
// Register a popstate listener that executes only once during the next history entry change
window.addEventListener('popstate', onPopStateCallback, {once: true});
We can register a callback for the next history entry(pop state) update

Switch to results Component `onSubmit` a search form in react hooks using react router

Hi I have a scenario where I put a search bar on the top nav so a user can search from anywhere in the app. How to do I switch to the results component once the user submits the search form? Here's my search component that populates the global state with search results but I can't manage to switch the view to the results component.
import React, { useState, useEffect, useContext } from 'react';
import axios from 'axios';
import { StateContext } from '../../StateContext';
import './SearchBar.scss';
import sprite from '../../assets/icons/sprite.svg';
function SearchBar() {
const [state, setState] = useContext(StateContext);
const [userInput, setUserInput] = useState('');
const [bookName, setBookName] = useState('');
useEffect(() => {
axios
.get(`https://www.googleapis.com/books/v1/volumes?q=${bookName}`)
.then((res) => {
let book_list = res.data.items;
setState({
book_list: book_list,
heading: 'Search Results'
});
})
.catch((err) => console.log(err));
}, [bookName]);
const findBook = (e) => {
e.preventDefault();
setBookName(userInput);
};
const onChange = (e) => {
setUserInput(e.target.value);
};
return (
<form className='searchbar' onSubmit={findBook}>
<input
type='search'
className='searchbar__input'
placeholder='Search for a book'
value={userInput}
onChange={onChange}
/>
<button className='searchbar__button'>
<svg className='searchbar__icon'>
<use xlinkHref={`${sprite}#icon-search`} />
</svg>
</button>
</form>
);
}
export default SearchBar;
Here's how I'm handling routing:
import React from 'react';
import Nav from './components/Nav/Nav';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Books from './containers/Books';
import Book from './containers/Book';
import { ContextController } from './StateContext';
function App() {
return (
<ContextController>
<Router>
<div className='app'>
<Nav />
<main>
<Switch>
<Route exact path='/' component={Books} />
<Route exact path='/book/:id' component={Book} />
</Switch>
</main>
</div>
</Router>
</ContextController>
);
}
export default App;
If you have a dedicated route for search results, try this in your ContextController
import { useHistory } from 'react-router-dom';
// later
const history = useHistory();
React.useEffect(() => {
if (state?.book_list?.length > 0) {
history.push('/search-results');
}
}, [state]);
Also, it is important to note that the Router should be on top of your Data Context;
Because if you want to access the history from the a tree, it needs to be wrapped in a Router, or else it will return undefined as a value for history
Here is a working codesandbox

How to properly refactor router components?

I'm building a to-do list app in React. With React-Router it has routes for "/all", "/today", "/week", "/inbox", and custom "/:projectId" tasks. They all render the same component <TaskList /> which accepts a couple of props:
Project ID, to add tasks to
Project Name, and
Tasks belonging to the respective project
I don't know how to properly refactor such code so that it's as efficient and DRY as possible. Here's my current attempt:
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import {
selectAllTasks,
selectTodayTasks,
selectWeekTasks,
selectProjectTasks
} from "../../redux/tasks.module";
import { selectCurrentProject } from "../../redux/projects.module";
import TaskList from "../task-list/task-list.component";
const projectId = 0; // Tasks entered will be added to Inbox
export const AllTasks = () => {
const tasks = useSelector(selectAllTasks);
return <TaskList projectName="All" projectId={projectId} tasks={tasks} />;
};
export const TodayTasks = () => {
const tasks = useSelector(selectTodayTasks);
return <TaskList projectName="Today" projectId={projectId} tasks={tasks} />;
};
export const WeekTasks = () => {
const tasks = useSelector(selectWeekTasks);
return (
<TaskList projectName="Next 7 Days" projectId={projectId} tasks={tasks} />
);
};
export const ProjectTasks = () => {
const { projectId } = useParams();
const { text } = useSelector(state => selectCurrentProject(state, projectId));
const tasks = useSelector(state => selectProjectTasks(state, projectId));
return <TaskList projectName={text} projectId={projectId} tasks={tasks} />;
};
And here's the page that calls them:
import { Switch, Route, useRouteMatch } from "react-router-dom";
import Header from "../../components/header/header.component";
import Sidebar from "../../components/sidebar/sidebar.component";
import {
AllTasks,
TodayTasks,
WeekTasks,
ProjectTasks
} from "../../components/filters/filters.component";
import useStyles from "./tasks.styles";
const TasksPage = () => {
const { path } = useRouteMatch();
const classes = useStyles();
return (
<div className={classes.container}>
<Header />
<Sidebar />
<Switch>
<Route exact path={`${path}/all`} component={AllTasks} />
<Route exact path={`${path}/today`} component={TodayTasks} />
<Route exact path={`${path}/week`} component={WeekTasks} />
<Route exact path={`${path}/:projectId`} component={ProjectTasks} />
</Switch>
</div>
);
};
export default TasksPage;
What's the most efficient way to structure this? And allow not only the hardcoded routes (i.e. all, today, week, etc.) but also the custom user project routes (/:projectId) to coexist and not have repeating code?
Thank you so much.

Resources