React with Enzyme test case - reactjs

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.

Related

Standards for passing props in React

I have been using React for close to a year know and understand a majority of the basics, however I have some questions regarding best practices (General or industry) when it comes to passing functions/ref/hooks and how it affects things like state flow and tests. I have been instantiating hooks such as useDispatch or useNavigation(React-router) in the App.tsx(js) file and then passing it down to all of my components who need to use it. I have been using this same concept for things like Axios and then within my components, I've been trying out passing MUI components(Grid, Card, etc) to my created component (i.e. LoginForm.tsx/js) where the initial rendering of the main component brings in those hooks instead of repeated instantiation throughout my project (Below for example). Is this breaking in standards or practices, such as SOLID OOP, and would this make testing harder down the line?
App.tsx
import { Dispatch, FC, Suspense, lazy } from "react";
import {
Navigate,
NavigateFunction,
Route,
Routes,
useNavigate,
useSearchParams,
} from "react-router-dom";
import {
HOMEPAGE,
LOGIN,
REDIRECT,
ROOM,
SEARCH,
SETUPROOM,
SIGNUP,
} from "./component/UI/Constatns";
import Layout from "./component/UI/Layout/Layout";
import { User } from "./types/types";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./store/store";
import { Theme, useMediaQuery, useTheme } from "#mui/material";
import axios from "axios";
import LoadingSpinner from "./component/UI/LoadingSpinner";
const Homepage = lazy(() => import("./pages/Homepage"));
const Login = lazy(() => import("./pages/Login"));
const Signup = lazy(() => import("./pages/Signup"));
const Room = lazy(() => import("./pages/Room"));
const Search = lazy(() => import("./pages/subpages/Search"));
const CreateRoom = lazy(() => import("./pages/subpages/CreateRoom"));
const App: FC = () => {
const USER: User = useSelector((state: RootState) => state.user);
const theme: Theme = useTheme();
const isMobile: boolean = useMediaQuery(theme.breakpoints.down("md"));
const [params] = useSearchParams();
const dispatch: Dispatch<any> = useDispatch();
const navigation: NavigateFunction = useNavigate();
return (
<Suspense fallback={<LoadingSpinner />}>
<Layout>
<Routes>
<Route
path={HOMEPAGE}
element={
<Homepage
user={USER}
isMobile={isMobile}
axios={axios}
dispatch={dispatch}
param={params}
/>
}
/>
<Route
path={SEARCH}
element={
<Search
axios={axios}
dispatch={dispatch}
params={params}
nav={navigation}
isMobile={isMobile}
/>
}
/>
<Route
path={ROOM}
element={
<Room
isMobile={isMobile}
nav={navigation}
dispatch={dispatch}
param={params}
/>
}
/>
<Route
path={SETUPROOM}
element={
<CreateRoom
params={params}
axios={axios}
nav={navigation}
isMobile={isMobile}
user={USER}
/>
}
/>
<Route
path={LOGIN}
element={
<Login
nav={navigation}
isMobile={isMobile}
params={params}
axios={axios}
dispatch={dispatch}
/>
}
/>
<Route
path={SIGNUP}
element={
<Signup nav={navigation} isMobile={isMobile} axios={axios} />
}
/>
<Route path={REDIRECT} element={<Navigate replace to={HOMEPAGE} />} />
</Routes>
</Layout>
</Suspense>
);
};
export default App;
Example of MUI hooks
import { Button, Card, CardContent, Grid, TextField } from "#mui/material";
import { AxiosStatic } from "axios";
import { Dispatch, FC, FormEvent, useEffect, useRef, useState } from "react";
import { NavigateFunction, NavLink } from "react-router-dom";
import { FETCHLOGIN, HOMEPAGE, LOGGEDIN } from "../component/UI/Constatns";
import { userActions } from "../store/user/user-slice";
import LoginForm from "../component/forms/login/LoginForm";
import classes from "../styles/LoginStyles.module.css";
const Login: FC<{
dispatch: Dispatch<any>;
isMobile: boolean;
params: URLSearchParams;
axios: AxiosStatic;
nav: NavigateFunction;
}> = ({ axios, dispatch, isMobile, params, nav }) => {
const [userPassword, setUserPassword] = useState<string>("");
const username = useRef<HTMLInputElement | undefined>();
const password = useRef<HTMLInputElement | undefined>();
const userSearchParam: string | null = params.get("username");
useEffect(() => {
if (userSearchParam) {
const fetchUser: (
axios: AxiosStatic,
username: string,
password: string
) => void = async (axios, username, password) => {
await axios
.post(FETCHLOGIN, { username: username, password: password })
.then((response) => {
dispatch(userActions.login({ username: response.data.username }));
nav(LOGGEDIN, { replace: true });
})
.catch(() => {
nav(HOMEPAGE, { replace: true });
});
};
fetchUser(axios, userSearchParam, userPassword);
}
}, [nav, axios, userPassword, userSearchParam, dispatch]);
const submitHandler: (e: FormEvent<HTMLFormElement>) => void = (e) => {
e.preventDefault();
setUserPassword(password.current?.value as string);
nav(`?username=${username.current?.value}`, { replace: true });
};
return (
<Grid className={classes.loginContainer} container>
<Card className={!isMobile ? classes.card : classes.mobCard}>
<div className={classes.cardHeader}>
<p>Please login</p>
</div>
<CardContent>
<LoginForm
Link={NavLink}
Submit={submitHandler}
TextField={TextField}
Button={Button}
Grid={Grid}
username={username}
password={password}
/>
</CardContent>
</Card>
</Grid>
);
};
export default Login;

React-Router-Dom <Link> not render page

I'm building a practice app that uses Unsplash to render users photos. I'm using React and Redux. With react-router-dom, I'm trying to follow the docs but I find it very confusing to set up. Here's what I have so far. When I click on a result out of a returned list of results from a search, I want it to render a user page profile.
index.js (make sure I have react-router-do set up correctly):
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
// import store from './app/store';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import reducers from "./app/reducers/rootReducer";
import * as serviceWorker from './serviceWorker';
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, storeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
Top component App
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Images from "./app/components/Images";
import Search from "./app/components/Search";
import UserProfile from "./app/components/UserProfile";
import "./App.css";
function App() {
return (
<>
<Search />
<Images />
<Router>
<Route link="/userProfile">
<UserProfile />
</Route>
</Router>
</>
);
}
export default App;
search (parent component to searchResults where exists):
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { queryAction } from "../actions/queryAction";
import SearchResults from "./SearchResults";
const Search = (props) => {
const [query, setQuery] = useState("");
console.log(props.searches);
const searchPhotos = async (e) => {
e.preventDefault();
console.log("submitting form");
props.queryAction(query);
};
const showUsers = (user, e) => {
e.preventDefault()
console.log(user)
};
return (
<>
<form className="form" onSubmit={searchPhotos}>
<label className="label" htmlFor="query">
{" "}
</label>
<input
type="text"
name="query"
className="input"
placeholder={`Try "dog" or "apple"`}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button type="submit" className="button">
Search
</button>
</form>
<SearchResults results={props.searches} showUsers={showUsers} />
</>
);
};
const mapStateToProps = (state) => {
return {
searches: state.searches,
};
};
const mapDispatchToProps = (dispatch) => {
return {
queryAction: (entry) => dispatch(queryAction(entry)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Search);
searchResults:
import React from "react";
import { BrowserRouter as Router, Link } from "react-router-dom";
import { getUserAction } from "../actions/getUserAction";
import { connect } from "react-redux";
const SearchResults = (props) => {
const { results } = props.results.searches;
const handleClick = (result, e) => {
e.preventDefault();
props.getUser(result.username);
};
return (
<>
{results &&
results.map((result, id) => {
return (
<div key={id}>
<Router>
<Link to="/userProfile" onClick={(e) => handleClick(result, e)}>
{result.username}
</Link>
</Router>
</div>
);
})}
</>
);
};
const mapDispatchToProps = (dispatch) => {
return {
getUser: (query) => dispatch(getUserAction(query)),
};
};
export default connect(null, mapDispatchToProps)(SearchResults);
and finally the UserProfile component:
import React from 'react';
import { connect } from 'react-redux';
const UserProfile = props => {
console.log(props)
return (
<div>
</div>
);
}
const mapStateToProps = state => {
return {
user: state.users
}
}
export default connect(mapStateToProps, null)(UserProfile);
app component
import React from "react";
import { Switch, Route } from "react-router-dom";
import Images from "./app/components/Images";
import Search from "./app/components/Search";
import UserProfile from "./app/components/UserProfile";
import "./App.css";
function App() {
return (
<>
<Search />
<Images />
<Switch>
<Route path="/userProfile/:username">
<UserProfile />
</Route>
</Switch>
</>
);
}
export default App;
SearchResults component
import React from "react";
import { Link } from "react-router-dom";
const SearchResults = (props) => {
const { results } = props.results.searches;
const handleClick = (result, e) => {
e.preventDefault();
props.getUser(result.username);
};
return (
<>
{results &&
results.map((result, id) => {
return (
<div key={id}>
<Link to={`/userProfile/${result.username}`}>
{result.username}
</Link>
</div>
);
})}
</>
);
};
export default SearchResults;
UserProfile component
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { getUserAction } from "../actions/getUserAction";
const UserProfile = props => {
useEffect(() => {
props.getUserAction(props.match.params.username)
},[])
console.log(props)
return (
<div>
{props.user
? <div>{user.username}</div>
: <div>Loading...</div>
}
</div>
);
}
const mapStateToProps = state => {
return {
user: state.users
}
}
const mapDispatchToProps = (dispatch) => {
return {
getUser: (query) => dispatch(getUserAction(query)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
Edit: Add a param to your link and remove the onclick. Update the Route to expect a :username param. You can access the param through props in UserProfile component.
Make sure to perform the action or access state when mounting the UserProfile component so you have some data when it renders.
Edit 2: Added UserProfile component to answer. You want to dispatch your action when the component is mounting. Also, set a ternary to show "Loading..." if state.user isn't done being fetched.

React router changing the url, but not rendering the view

I am doing a typescript assignment, which is an app for a doctor to add patients, diagnoses and so on. I am using react-router. The router is changing the URL, but not rendering the patient view for some reason. I have been trying to figure this out for a while now. Can someone push me into the right direction? Thank you.
App.tsx
import React, { useState } from "react";
import axios from "axios";
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";
import { Button, Divider, Header, Container } from "semantic-ui-react";
import { apiBaseUrl } from "./constants";
import { useStateValue } from "./state";
import { Patient } from "./types";
import PatientListPage from "./PatientListPage";
import PatientPage from "./components/PatientPage";
const App: React.FC = () => {
//const [, dispatch] = useStateValue();
const [{ patient }, dispatch] = useStateValue();
const [page, setPage] = useState('');
React.useEffect(() => {
axios.get<void>(`${apiBaseUrl}/ping`);
const fetchPatientList = async () => {
try {
const { data: patientListFromApi } = await axios.get<Patient[]>(
`${apiBaseUrl}/api/patients`
);
dispatch({ type: "SET_PATIENT_LIST", payload: patientListFromApi });
} catch (e) {
console.error(e);
}
};
fetchPatientList();
}, [dispatch]);
const showPatient = async (id: string) => {
try {
const { data: patientFromApi } = await axios.get<Patient>(`${apiBaseUrl}/api/patients/${id}`);
dispatch({ type: "GET_PATIENT", payload: patientFromApi });
setPage(patientFromApi.id);
console.log('patient', patient);
} catch (error) {
console.log(error.message);
}
}
return (
<div className="App">
<Router>
<Container>
<Header as="h1">Patientor</Header>
<Button as={Link} to="/" primary>
Home
</Button>
<Divider hidden />
<Switch>
<Route path="/">
<PatientListPage showPatient={showPatient} />
</Route>
<Route path={`/${page}`} >
<PatientPage />
</Route>
</Switch>
</Container>
</Router>
</div>
);
};
export default App;
PatientListPage.tsx
import React from "react";
import axios from "axios";
import { Container, Table, Button } from "semantic-ui-react";
import { PatientFormValues } from "../AddPatientModal/AddPatientForm";
import AddPatientModal from "../AddPatientModal";
import { Patient } from "../types";
import { apiBaseUrl } from "../constants";
import HealthRatingBar from "../components/HealthRatingBar";
import { useStateValue } from "../state";
import { Link } from "react-router-dom";
const PatientListPage: React.FC<{ showPatient: any }> = ({ showPatient }) => {
const [{ patients, patient }, dispatch] = useStateValue();
const [modalOpen, setModalOpen] = React.useState<boolean>(false);
const [error, setError] = React.useState<string | undefined>();
const openModal = (): void => setModalOpen(true);
const closeModal = (): void => {
setModalOpen(false);
setError(undefined);
};
const submitNewPatient = async (values: PatientFormValues) => {
try {
const { data: newPatient } = await axios.post<Patient>(
`${apiBaseUrl}/api/patients`,
values
);
dispatch({ type: "ADD_PATIENT", payload: newPatient });
closeModal();
} catch (e) {
console.error(e.response.data);
setError(e.response.data.error);
}
};
return (
<div className="App">
<Container textAlign="center">
<h3>Patient list</h3>
</Container>
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Gender</Table.HeaderCell>
<Table.HeaderCell>Occupation</Table.HeaderCell>
<Table.HeaderCell>Health Rating</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{Object.values(patients).map((patient: Patient) => (
<Table.Row key={patient.id} onClick={() => showPatient(patient.id)}>
<Link to={`/${patient.id}`}>
<Table.Cell>{patient.name}</Table.Cell>
</Link>
<Table.Cell>{patient.gender}</Table.Cell>
<Table.Cell>{patient.occupation}</Table.Cell>
<Table.Cell>
<HealthRatingBar showText={false} rating={1} />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<AddPatientModal
modalOpen={modalOpen}
onSubmit={submitNewPatient}
error={error}
onClose={closeModal}
/>
<Button onClick={() => openModal()}>Add New Patient</Button>
</div>
);
};
export default PatientListPage;
PatientPage.tsx
import React, { useState } from "react";
import { Patient } from '../types';
import { useStateValue } from "../state";
const PatientPage: React.FC = () => {
const [{ patient }, dispatch] = useStateValue();
return (
<>
name: {patient.name}
ssn: {patient.ssn}
occupation: {patient.occupation}
</>
)
}
export default PatientPage
It might be because you have not used the exact keyword, so its rendering the PatientListPage component instead.
<Switch>
<Route exact path="/">
<PatientListPage showPatient={showPatient} />
</Route>
<Route path={`/${page}`}>
<PatientPage />
</Route>
</Switch>
More information here
First Solution and Best Solution:
If you use are using React Router 5.3.x, check whether it is 5.3.3 in your package.json file.
If it is not 5.3.3 uninstall the last version then install the bug-free version which has been resolved by John and updated in version 5.3.3.
npm uninstall -S react-router-dom
npm install -S react-router-dom#5.3.3
Second Solution:
React has launched its StrictMode in its latest update.
you can see it in an index.js file
index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
So here your React Router is in the child component. And we have to make it a parent component.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<React.StrictMode>
<App />
</React.StrictMode>
</BrowserRouter>
);
Third Solution:
Remove the Strict mode from the index.js file
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);

Navbar is not re-rendering on router location change

I want to display userName and changed the button text to 'logout' from 'login' in Navbar(nav component) when user is logged in. I tired to re-render the Nav Component by pushing the router history location to homepage on user logged in.
this.props.history.push('/')
Then User can successfully log in, user data is saved in localStorage and the router location is changed. data is saved in localStorage, but the component is not re-rendering.
Here is what I have tried.
tried to downgrade 'connected-react-router' to 'v6.0.0' but it threw me some errors. so put it back to the latest version '^6.4.0'.
tried to add Navbar code in App.js which is the parents component.
But the app.js itself is not re-rendering either on user logged in.
app.js
import React from 'react';
import { connect } from 'react-redux';
/* --- Components --- */
import Nav from './src/components/nav';
import Loader from './src/shared/loader';
import './styles/main.scss';
const FlashMessagesContainer = Loader({
loader: () =>
import('./src/shared/flassMessagesContainer' /* webpackChunkName: 'FlashMessagesContainer' */),
});
const App = (props, { isOnModal }) => (
<div id="app">
<Nav />
{!isOnModal && (
<div className="flex justify-center">
<FlashMessagesContainer />
</div>
)}
{props.children}
</div>
);
const mapPropsToState = state => ({
isOnModal: state.modal.show,
});
export default connect(
mapPropsToState,
null,
)(App);
nav.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
/* --- Components --- */
import {
isLoggedIn,
getCompanyName,
clearLocalStorage,
} from '../../localStorage';
class Nav extends Component {
handleUserLogout = async ev => {
ev.preventDefault();
await clearLocalStorage();
return this.props.history.push('/login');
};
render() {
return (
<div className="nav">
{isLoggedIn() ? (
<div className="flex">
<p className="mr3 mt2">
안녕하세요. <span className="b">{getCompanyName()}</span>
 님,
</p>
<button
type="button"
className="login-btn td-none c-text br f-mini"
onClick={this.handleUserLogout}
>
로그아웃
</button>
</div>
) : (
<Link className="login-btn td-none c-text br f-mini" to="/login">
로그인
</Link>
)}
</div>
);
}
}
export default withRouter(Nav);
localStorage.js
export const getToken = () => localStorage.token;
export const getCompanyName = () => localStorage.companyName;
export const isLoggedIn = () => !!localStorage.token;
export const saveUserNameAndToken = userData => {
localStorage.setItem('token', userData.token);
localStorage.setItem('companyName', userData.companyName);
};
export const clearLocalStorage = () => localStorage.clear();
index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter as Router } from 'connected-react-router';
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import configureStore, { history } from './store';
import Routes from './routes';
const theme = createMuiTheme({
...
});
const store = configureStore();
const root = document.createElement('div');
document.body.appendChild(root);
render(
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<Router history={history}>
<Routes />
</Router>
</MuiThemeProvider>
</Provider>,
root,
);
routes.js
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { hot, setConfig } from 'react-hot-loader';
import { withRouter } from 'react-router';
/* --- Components --- */
import App from './app';
import Loader from './src/shared/loader';
const Home = Loader({
loader: () =>
import('./src/components/home/homeContainer' /* webpackChunkName: 'Home' */),
});
...
const routes = props => (
<div>
<App history={props.history} />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" render={props => <Login {...props} />} />
<Route path="/users/account" component={Users} />
<Route component={NoMatch} />
</Switch>
</div>
);
const Routes =
!module.hot || process.env.NODE_ENV === 'production'
? routes
: hot(module)(routes);
export default withRouter(Routes);
I am expecting Nav component to be re-rendered when router location changes.
react: ^16.8.6
react-dom: ^16.8.6
react-loadable: ^5.5.0
react-redux: ^6.0.0
react-router-dom: ^4.3.1
redux: ^4.0.0
connected-react-router: ^6.4.0
I fixed it by saving 'isLoggedIn' state in redux store. And to persist the state from redux store on page refresh, I used 'redux-persist'.
Now Nav component is re-rendering on router location change and the userName data is saved even on page refresh.
authReducer.js
const initialState = {
isLoggedIn: false,
userName: '',
};
const auth = (state = initialState, action) => {
switch (action.type) {
case types.USER_LOGIN:
return {
...state,
isLoggedIn: true,
userName: action.payload,
};
case types.USER_LOGOUT:
return {
...state,
isLoggedIn: false,
userName: '',
};
default:
return state;
}
};
nav.js
...
{this.props.isLoggedIn ? (
...
) : (
...
)}
...
const mapStateToProps = state => ({
isLoggedIn: state.auth.isLoggedIn,
userName: state.auth.userName,
});
const mapDispatchToProps = dispatch => ({
userLogout: () => dispatch(userLogout()),
});
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(Nav),
);
[ redux-persist SetUp ]
store.js
import { applyMiddleware, createStore } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import { routerMiddleware } from 'connected-react-router';
import createBrowserHistory from 'history/createBrowserHistory';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import createRootReducer from './src/reducers';
export const history = createBrowserHistory();
const persistConfig = {
key: 'root',
storage,
};
const persistedReducer = persistReducer(
persistConfig,
createRootReducer(history),
);
const store = createStore(
persistedReducer,
composeWithDevTools(
applyMiddleware(
routerMiddleware(history),
thunkMiddleware,
createLogger({
predicate: () => process.env.NODE_ENV === 'development',
collapsed: true,
}),
),
),
);
export const persistor = persistStore(store);
export default store;
index.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter as Router } from 'connected-react-router';
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import { PersistGate } from 'redux-persist/lib/integration/react';
import store, { history, persistor } from './store';
import Routes from './routes';
import Loading from './src/shared/loading';
const theme = createMuiTheme({
palette: {
primary: { main: '#AE9A65' },
secondary: { main: '#ee9105' },
success: { main: '#43A047' },
warning: { main: '#FFA000' },
error: { main: '#ed4337' },
info: { main: '#2196F3' },
},
});
const root = document.createElement('div');
document.body.appendChild(root);
render(
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<MuiThemeProvider theme={theme}>
<Router history={history}>
<Routes />
</Router>
</MuiThemeProvider>
</PersistGate>
</Provider>,
root,
);

React Router v4 and loadable-components always rendering same Component even though the route matches other

I've got a Standard React-Redux Application with react router v4, webpack 4 and I'm trying to perform lazy loading of components via loadable-components library, so webpack can create chunks and load them on demand. The problems seems it's always rendering the Dashboard (refer code below) Component inside the <Switch>, no matter the route.
I don't understand the cause. I found a similar problem: React-Router v4 rendering wrong component but matching correctly but it follows a different pattern and I can't or don't understand how to apply that solution into my issue.
I'm posting The main AppContainer and the routes file:
AppContainer.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import ReduxToastr from 'react-redux-toastr'
import 'react-redux-toastr/src/styles/index.scss'
import { Offline, Online } from 'react-detect-offline'
import Footer from 'components/Footer/'
import { Modal, ModalHeader, ModalBody } from 'reactstrap'
import { Translate } from 'react-redux-i18n'
import { PersistGate } from 'redux-persist/integration/react'
import { Router, Switch, Route } from 'react-router-dom'
import PrivateRoute from 'components/PrivateRoute'
import { Dashboard, PasswordReset, PasswordResetEdit, SignIn } from 'routes'
// Layout
import CoreLayout from 'layouts/CoreLayout'
class AppContainer extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
persistor: PropTypes.object.isRequired, // redux-persist
store : PropTypes.object.isRequired
}
render () {
const { history, store, persistor } = this.props
const opened = true
const toastrDefaultTimeout = 8000
const newToastrAlwaysOnTop = false
return (
<div>
<Online>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<div style={{ height: '100%' }}>
<ReduxToastr
timeOut={toastrDefaultTimeout}
newestOnTop={newToastrAlwaysOnTop}
preventDuplicates
position='top-right'
transitionIn='fadeIn'
transitionOut='fadeOut'
progressBar />
<Router history={history}>
<CoreLayout {...this.props} >
<Switch>
<Route exact path='/sign-in' component={SignIn} />
<Route exact path='/passwords/new' component={PasswordReset} />
<Route exact path='/passwords/edit' component={PasswordResetEdit} />
<PrivateRoute exact path='/' component={Dashboard} persistor={persistor} />
</Switch>
</CoreLayout>
</Router>
</div>
</PersistGate>
</Provider>
</Online>
<Offline>
<div className='app'>
<div className='app-body'>
<main className='main align-items-center align-self-center'>
<div className='container'>
<div className='animated fadeIn'>
<Modal isOpen={opened} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}><Translate value={`views.shared.no-internet.title`} /></ModalHeader>
<ModalBody>
<div className='card-block'>
<div className='row'>
<div className='col-sm-12'>
<Translate value='views.shared.no-internet.description' />
</div>
</div>
</div>
</ModalBody>
</Modal>
</div>
</div>
</main>
</div>
<Footer />
</div>
</Offline>
</div>
)
}
}
export default AppContainer
routes/index.js:
// We only need to import the modules necessary for initial render
import loadable from 'loadable-components'
import { hot } from 'react-hot-loader'
// Component split-code (lazy load)
let Dashboard,
SignIn,
PasswordReset,
PasswordResetEdit
// Needed for HMR
if (__DEV__) {
Dashboard = hot(module)(loadable(() => import('./Dashboard')))
PasswordReset = hot(module)(loadable(() => import('./PasswordReset')))
PasswordResetEdit = hot(module)(loadable(() => import('./PasswordResetEdit')))
SignIn = hot(module)(loadable(() => import('./SignIn')))
} else {
Dashboard = loadable(() => import('./Dashboard'))
SignIn = loadable(() => import('./SignIn'))
PasswordReset = loadable(() => import('./PasswordReset'))
PasswordResetEdit = loadable(() => import('./PasswordResetEdit'))
}
export { Dashboard, PasswordReset, PasswordResetEdit, SignIn }
And for anyone curious, here is the PrivateRoute component:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Route, Redirect } from 'react-router-dom'
import { connect } from 'react-redux'
import { validateSession } from 'modules/sessions/session'
const mapStateToProps = state => ({
session: state.session
})
const mapDispatchToProps = (dispatch) => {
return {
validateSession: () => { dispatch(validateSession()) }
}
}
class PrivateRoute extends Component {
componentDidMount () {
this.props.validateSession(this.props.persistor)
}
render () {
const { component: Component, session, ...rest } = this.props
return (
<Route {...rest} render={(routeProps) =>
this.props.session.userSignedIn ? <Component {...routeProps} />
: <Redirect to={{
pathname: '/sign-in',
state: { from: routeProps.location }
}} />
}
/>)
}
}
PrivateRoute.propTypes = {
component: PropTypes.func.isRequired,
persistor: PropTypes.object.isRequired,
session: PropTypes.object.isRequired,
validateSession: PropTypes.func.isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute)
If I remove the if (_DEV_) block in routes/index.js and always do it like the 'else' block everything works okay, however I lose HMR, meaning I have to refresh the browser to see code changes take effect.
Edit
For anyone wandering where the history prop comes from:
Main app entry point (in webpack.config.js). src/main.js:
import React from 'react'
import ReactDOM from 'react-dom'
import createStore from './store/createStore'
import { hot } from 'react-hot-loader'
import AppContainer from './containers/AppContainer'
import { persistStore } from 'redux-persist'
// ========================================================
// Store Instantiation
// ========================================================
const initialState = window.___INITIAL_STATE__
const { history, store } = createStore(initialState)
// begin periodically persisting the store
let persistor = persistStore(store)
// ========================================================
// Render Setup
// ========================================================
const MOUNT_NODE = document.getElementById('root')
let render = () => {
ReactDOM.render(
<AppContainer store={store} persistor={persistor} history={history} />,
MOUNT_NODE
)
}
// This code is excluded from production bundle
if (__DEV__) {
if (module.hot) {
// Development render functions
const renderApp = render
const renderError = (error) => {
const RedBox = require('redbox-react').default
ReactDOM.render(<RedBox error={error} />, MOUNT_NODE)
}
// Wrap render in try/catch
render = () => {
try {
renderApp()
} catch (error) {
console.error(error)
renderError(error)
}
}
// Setup hot module replacement
module.hot.accept('./containers/AppContainer', () =>
render(require('./containers/AppContainer').default)
)
}
}
// ========================================================
// Go!
// ========================================================
export default hot(module)(render)
render()
createStore.js:
import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import { apiMiddleware } from 'redux-api-middleware'
import { createLogger } from 'redux-logger'
import makeRootReducer from './reducers'
import { routerMiddleware } from 'react-router-redux'
// I18n
import { syncTranslationWithStore, loadTranslations, setLocale } from 'react-redux-i18n'
import { translationsObject } from 'translations/index'
// Router history
import createHistory from 'history/createBrowserHistory'
// Raven for Sentry
import Raven from 'raven-js'
import createRavenMiddleware from 'raven-for-redux'
export default (initialState = {}) => {
// ======================================================
// Middleware Configuration
// ======================================================
const logger = createLogger()
const history = createHistory()
const historyMiddleware = routerMiddleware(history)
const middleware = [historyMiddleware, apiMiddleware, thunk, logger]
if (__PROD__) {
Raven.config(`${__SENTRY_DSN__}`).install()
middleware.push(createRavenMiddleware(Raven))
}
// ======================================================
// Store Enhancers
// ======================================================
const enhancers = []
let composeEnhancers = compose
const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
if (typeof composeWithDevToolsExtension === 'function') {
composeEnhancers = composeWithDevToolsExtension
}
// ======================================================
// Store Instantiation and HMR Setup
// ======================================================
const store = createStore(
makeRootReducer(),
initialState,
composeEnhancers(
applyMiddleware(...middleware),
...enhancers
)
)
store.asyncReducers = {}
// DEPRECATED in react-router v4: To unsubscribe, invoke `store.unsubscribeHistory()` anytime
// store.unsubscribeHistory = browserHistory.listen(updateLocation(store))
if (module.hot) {
const reducers = require('./reducers').default
module.hot.accept('./reducers', () => {
store.replaceReducer(reducers(store.asyncReducers))
})
}
syncTranslationWithStore(store)
store.dispatch(loadTranslations(translationsObject))
store.dispatch(setLocale('es_AR'))
return { history, store }
}

Resources