React Context API + withRouter - can we use them together? - reactjs

I built a large application where a single button on the navbar opens a modal.
I'm keeping track of the modalOpen state using context API.
So, user clicks button on navbar. Modal Opens. Modal has container called QuoteCalculator.
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.props.history.push('/quote');
// ..
};
render() {
//...
return(<Question {...props} next={this.onSubmit} />;)
}
}
export default withRouter(QuoteCalculator);
Now, everything works as expected. When the user submits, I go to the right route. I just see the following warning on the console
index.js:1446 Warning: withRouter(QuoteCalculator): Function
components do not support contextType.
I'm tempted to ignore the warning, but I don't think its a good idea.
I tried using Redirect alternatively. So something like
QuoteCalculator looks as follows:
class QuoteCalculator extends React.Component {
static contextType = ModalContext;
// ...
onSubmit = () => {
// ...
this.context.toggleModal();
this.setState({done: true});
// ..
};
render() {
let toDisplay;
if(this.state.done) {
toDisplay = <Redirect to="/quote"/>
} else {
toDipslay = <Question {...props} next={this.onSubmit} />;
}
return(<>{toDisplay}</>)
}
}
export default QuoteCalculator;
The problem with this approach is that I kept on getting the error
You tried to redirect to the same route you're currently on
Also, I'd rather not use this approach, just because then I'd have to undo the state done (otherwise when user clicks button again, done is true, and we'll just get redirected) ...
Any idea whats going on with withRouter and history.push?
Here's my app
class App extends Component {
render() {
return (
<Layout>
<Switch>
<Route path="/quote" component={Quote} />
<Route path="/pricing" component={Pricing} />
<Route path="/about" component={About} />
<Route path="/faq" component={FAQ} />
<Route path="/" exact component={Home} />
<Redirect to="/" />
</Switch>
</Layout>
);
}
}

Source of the warning
Unlike most higher order components, withRouter is wrapping the component you pass inside a functional component instead of a class component. But it's still calling hoistStatics, which is taking your contextType static and moving it to the function component returned by withRouter. That should usually be fine, but you've found an instance where it's not. You can check the repo code for more details, but it's short so I'm just going to drop the relevant lines here for you:
function withRouter(Component) {
// this is a functional component
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps}
{...routeComponentProps}
ref={wrappedComponentRef}
/>
)}
/>
);
};
// ...
// hoistStatics moves statics from Component to C
return hoistStatics(C, Component);
}
It really shouldn't negatively impact anything. Your context will still work and will just be ignored on the wrapping component returned from withRouter. However, it's not difficult to alter things to remove that problem.
Possible Solutions
Simplest
Since all you need in your modal is history.push, you could just pass that as a prop from the modal's parent component. Given the setup you described, I'm guessing the modal is included in one place in the app, fairly high up in the component tree. If the component that includes your modal is already a Route component, then it has access to history and can just pass push along to the modal. If it's not, then wrap the parent component in withRouter to get access to the router props.
Not bad
You could also make your modal component a simple wrapper around your modal content/functionality, using the ModalContext.Consumer component to pass the needed context down as props instead of using contextType.
const Modal = () => (
<ModalContext.Consumer>
{value => <ModalContent {...value} />}
</ModalContext.Consumer>
)
class ModalContent extends React.Component {
onSubmit = () => {
// ...
this.props.toggleModal()
this.props.history.push('/quote')
// ..
}
// ...
}

Related

ReactJS -How to create multistep component/form with single path using React Router

I want to switch between components after the user entered the requested info.
Components that will be shown to user by this order:
{MobileNum } Enter mobile number
{IdNumber } ID number
{CreatePassword } Create Password
When all these steps are completed the browser will switch to the home page.
The user must not be able to move between pages until he filled each request in each component.
Now I want a better way with router as if I had 3-4 components inside Login, and must be in a secured whey, also the user must not be able to switch components manually through the URL.
import React, { Component } from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
class SignUp extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
//// Here needs to know how to navigate to each component on its turn
<Route path='/' component={MobileNum} />
<Route path='/' component={IdNumber} />
<Route path='/' component={CreatePassword } />
</Switch>
</Router>
</div>
);
}
}
export default SignUp ;
I searched the web in reactrouter.com and many others as here for a clean solution but found no answer.
Any Idea what's the best way to do it ?
Thanks
Since router variable like location are immutable, conditional rendering itself would be better option, you can try switch if you don't want to use if else.
I have given an example below, you have to fire that afterSubmit when values are submitted in each component .If you use redux, you could implement it better as you can store the value in redux state and set it directly from each component using dipatch.
//App.js
import React, { useState } from 'react';
import MobileNum from './MobileNum.jsx';
import IdNumber from './IdNum.jsx';
import CreatePassword from './createPassword .jsx';
function App (){
const [stage,setStage]= useState(1);
switch(stage){
case 2:
return <IdNumber afterSubmit={setStage.bind(null,3)}/>
break;
case 3:
return <CreatePassword afterSubmit={setStage.bind(null,4)} />
case 4:
return <Home />
break;
default:
return <MobileNum afterSubmit={setStage.bind(null,2)}/>
}
}
export default App;
//Root
import React, { Component } from 'react';
import App from './App.jsx';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch,
} from 'react-router-dom';
class Login extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
<Route path='/' component={App} />
</Switch>
</Router>
</div>
);
}
}
//Add on - Sign up form class based
class SignUp extends React.Component {
constructor(props) {
super(props);
this.state = { stage: 1 };
}
render() {
switch (this.state.stage) {
case 2:
return <IdNumber afterSubmit={() => this.setState({ stage: 3 })} />;
break;
case 3:
return <CreatePassword afterSubmit={() => this.setState({ stage: 4 })} />;
case 4:
return <Home />;
break;
default:
return <MobileNum afterSubmit={() => this.setState({ stage: 2 })} />;
}
}
}
It will take special handling in React Router to meet your security requirements. I personally would load the multi-step wizard on one URL rather than changing the URL for each step as this simplifies things and avoids a lot of potential issues. You can get the setup that you want, but it is much more difficult than it needs to be.
Path-Based Routing
I am using the new React Router v6 alpha for this answer, as it makes nested routes much easier. I am using /signup as the path to our form and URLs like /signup/password for the individual steps.
Your main app routing might look something like this:
import { Suspense, lazy } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Header from "./Header";
import Footer from "./Footer";
const Home = lazy(() => import("./Home"));
const MultiStepForm = lazy(() => import("./MultiStepForm/index"));
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup/*" element={<MultiStepForm/>} />
</Routes>
<Footer />
</BrowserRouter>
</Suspense>
);
}
You'll handle the individual step paths inside the MultiStepForm component. You can share certain parts of the form across all steps. The part which is your Routes should just be the part that is different, ie. the form fields.
Your nested Routes object inside the MultiStepForm is essentially this:
<Routes>
<Route path="/" element={<Mobile />} />
<Route path="username" element={<Username />} />
<Route path="password" element={<Password />} />
</Routes>
But we are going to need to know the order of our route paths in order to handle "Previous" and "Next" links. So in my opinion it makes more sense to generate the routes based on a configuration array. In React Router v5 you would pass your config as props to a <Route/>. In v6 you can skip that step and use object-based routing.
import React, { lazy } from "react";
const Mobile = lazy(() => import("./Mobile"));
const Username = lazy(() => import("./Username"));
const Password = lazy(() => import("./Password"));
/**
* The steps in the correct order.
* Contains the slug for the URL and the render component.
*/
export const stepOrder = [
{
path: "",
element: <Mobile />
},
{
path: "username",
element: <Username />
},
{
path: "password",
element: <Password />
}
];
// derived path order is just a helper
const pathOrder = stepOrder.map((o) => o.path);
Note that these components are called with no props. I am assuming that they get all of the information that they need through contexts. If you want to pass down props then you will need to refactor this.
import { useRoutes } from "react-router-dom";
import { stepOrder } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
return (
<div>
<Progress />
{stepElement}
</div>
);
}
Current Position
This is the part where things start to become convoluted. It seems that useRouteMatch has been removed in v6 (for now at least).
We can access the matched wildcard portion on the URL using the "*" property on the useParams hook. But this feels like it might be a bug rather than an intentional behavior, so I'm concerned that it could change in a future release. Keep that in mind. But it does work currently.
We can do this inside of a custom hook so that we can derive other useful information.
export const useCurrentPosition = () => {
// access slug from the URL and find its step number
const urlSlug = useParams()["*"]?.toLowerCase();
// note: will be -1 if slug is invalid, so replace with 0
const index = urlSlug ? pathOrder.indexOf(urlSlug) || 0 : 0;
const slug = pathOrder[index];
// prev and next might be undefined, depending on the index
const previousSlug = pathOrder[index - 1];
const nextSlug = pathOrder[index + 1];
return {
slug,
index,
isFirst: previousSlug === undefined,
isLast: nextSlug === undefined,
previousSlug,
nextSlug
};
};
Next Step
The user must not be able to move between pages until he filled each request in each component.
You will need some sort of form validation. You could wait to validate until the user clicks the "Next" button, but most modern websites choose to validate the data every time that the form changes. Packages like Formik and Yup are a huge help with this. Check out the examples in the Formik Validation docs.
You will have an isValid boolean which tells you when the user is allowed to move on. You can use that to set the disabled prop on the "Next" button. That button should have type="submit" so that its clicks can be handled by the onSubmit action of the form.
We can make that logic into a PrevNextLinks component which we can use in each form. This component uses the formik context so it must be rendered inside of a <Formik/> form.
We can use the info from our useCurrentPosition hook to render a Link to the previous step.
import { useFormikContext } from "formik";
import { Link } from "react-router-dom";
import { useCurrentPosition } from "./order";
/**
* Needs to be rendered inside of a Formik component.
*/
export default function PrevNextLinks() {
const { isValid } = useFormikContext();
const { isFirst, isLast, previousSlug } = useCurrentPosition();
return (
<div>
{/* button links to the previous step, if exists */}
{isFirst || (
<Link to={`/form/${previousSlug}`}>
<button>Previous</button>
</Link>
)}
{/* button to next step -- submit action on the form handles the action */}
<button type="submit" disabled={!isValid}>
{isLast ? "Submit" : "Next"}
</button>
</div>
);
}
Here's an example of how one step might look:
import { Formik, Form, Field, ErrorMessage } from "formik";
import React from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import Yup from "yup";
import "yup-phone";
import PrevNextLinks from "./PrevNextLinks";
import { useCurrentPosition } from "./order";
import { saveStep } from "../../store/slice";
const MobileSchema = Yup.object().shape({
number: Yup.string()
.min(10)
.phone("US", true)
.required("A valid US phone number is required")
});
export default function MobileForm() {
const { index, nextSlug, isLast } = useCurrentPosition();
const dispatch = useDispatch();
const navigate = useNavigate();
return (
<div>
<h1>Signup</h1>
<Formik
initialValues={{
number: ""
}}
validationSchema={MobileSchema}
validateOnMount={true}
onSubmit={(values) => {
// I'm iffy on this part. The dispatch and the navigate will occur simoultaneously,
// so you should not assume that the dispatch is finished before the target page is loaded.
dispatch(saveStep({ values, index }));
navigate(isLast ? "/" : `/signup/${nextSlug}`);
}}
>
{({ errors, touched }) => (
<Form>
<label>
Mobile Number
<Field name="number" type="tel" />
</label>
<ErrorMessage name="number" />
<PrevNextLinks />
</Form>
)}
</Formik>
</div>
);
}
Preventing Access via URL
The user must not be able to switch components manually through the URL.
We need to redirect the user if they attempt to access a page which they are not permitted to view. The Redirects (Auth) example in the docs should give you some ideas on how this is implemented. This PrivateRoute component in particular:
// A wrapper for <Route> that redirects to the login
// screen if you're not yet authenticated.
function PrivateRoute({ children, ...rest }) {
let auth = useAuth();
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
But what is your equivalent version of useAuth?
Idea: Look at the Current Progress
We could allow the visitor to view their current step and any previously entered steps. We look to see if the user is allowed to view the step which they are attempting to access. If yes, we load that content. If no, you can redirect them to their correct step or to the first step.
You would need to know what progress has been completed. That information needs to exist somewhere higher-up in the chain like localStorage, a parent component, Redux, a context provider, etc. Which you choose is up to you and there will be some differences. For example using localStorage will persist a partially-completed form while the others will not.
Where you store is less important that What you store. We want to allow backwards navigation to previous steps and forwards navigation if going to a previously-visited step. So we need to know which steps we can access and which we can't. The order matters so we want some sort of array. We would figure out the maximum step which we are allowed to access and compare that to the requested step.
Your component might look like this:
import { useRoutes, Navigate } from "react-router-dom";
import { useSelector } from "../../store";
import { stepOrder, useCurrentPosition } from "./order";
import Progress from "./Progress";
export default function MultiStepForm() {
const stepElement = useRoutes(stepOrder);
// attempting to access step
const { index } = useCurrentPosition();
// check that we have data for all previous steps
const submittedStepData = useSelector((state) => state.multiStepForm);
const canAccess = submittedStepData.length >= index;
// redirect to first step
if (!canAccess) {
return <Navigate to="" replace />;
}
// or load the requested step
return (
<div>
<Progress />
{stepElement}
</div>
);
}
CodeSandbox Link. (Note: Most of the code in the three step forms can and should be combined).
This is all getting rather complicated, so let's try something simpler.
Idea: Require that the URL Be Accessed from a Previous/Next Link
We can use the state property of a location to pass through some sort of information that lets us know that we've come from the correct place. Like {fromForm: true}. Your MultiStepForm can redirect all traffic that lacks this property to the first step.
const {state} = useLocation();
if ( ! state?.fromForm ) {
return <Navigate to="" replace state={{fromForm: true}}/>
}
You would make sure that all of your Link and navigate actions inside of the form are passing this state.
<Link to={`/signup/${previousSlug}`} state={{fromForm: true}}>
<button>Previous</button>
</Link>
navigate(`/signup/${nextSlug}`, {state: { fromForm: true} });
With No Path Change
After having written quite a lot of code and explanation about authenticating a path, I've realized that you haven't explicitly said that the path needs to change.
I just need to use react-router-dom properties to navigate.
So you could make use of the state property on the location object to control the current step. You pass the state through your Link and navigate the same as above, but with an object like {step: 1} instead of {fromForm: true}.
<Link to="" replace state={{step: 2}}>Next</Link>
You can majorly simplify your code by doing this. Though we come back to a fundamental question of why. Why use React Router if the important information is a state? Why not just use a local component state (or Redux state) and call setState when you click on the "Next" button?
Here's a good article with a fully-implemented code example using local state and the Material UI Stepper component:
Build a multi-step form with React Hooks, Formik, Yup and MaterialUI by Vu Nguyen
CodeSandbox
When all these steps are completed the browser will switch to the home page.
There are two ways to handle your final redirect to the home page. You can conditionally render a <Navigate/> component (<Redirect/> in v5) or you can call navigate() (history.push() in v5) in response to a user action. Both versions are explained in detail in the React Router v6 Migration guide.
I don't think adding React Router or any library changes the way how we solve a problem in React.
Your earlier approach was fine. You could wrap all it in a new component, like,
class MultiStepForm extends React.Component {
constructor(props) {
super(props);
this.state = {
askMobile: true,
};
};
askIdentification = (passed) => {
if (passed) {
this.setState({ askMobile: false });
}
};
render() {
return (
<div className='App-div'>
<Header />
{this.state.askMobile ? (
<MobileNum legit={this.askIdentification} />
) : (
<IdNumber />
)}
</div>
);
}
}
Then use this component on your Route.
...
<Switch>
<Route path='/' component={MultiStepForm} />
// <Route path='/' component={MobileNum} />
// <Route path='/' component={IdNumber} />
// <Route path='/' component={CreatePassword } />
</Switch>
...
Now how you'd like to move on with this is a completely new question.
Also, I have corrected the spelling of askIdentification.

React call parent method from child of a child

i'm facing a small problem with my react app.
I'm using bluprintjs Toaster, and i need to display them on top of all other component, no matter what. Like this if there is an error during login or logout the user will always see the toast even if there is a redirection.
My problem is, that i have a middle component that is used to protect access to unAuthenticated user.
On my app class i have a ref to the Toaster and can easily call renderToaster to display a toast. So the method is working correctly.
But when i pass it to my ProtectedRoute and then to MyForm Component i can't call it in the MyFrom component.
From App -> ProtectedRoute -> MyForm if i print this.props i can see the renderToaster() Method, but i think the link from MyFrom -> ProtectedRoute -> App is somehow broken because on MyFrom i have the this.toaster is undefined error.
How can i call my parent parent method. Or how can i create a link between app and MyForm compenent passing through ProtectedRoute?
Thank you for your help.
My App class:
class App extends Component {
renderToaster(intent, message) {
this.toaster.show({
intent: intent,
message: message
});
}
<React.Fragment>
<NavBarComponent />
<Switch>
<ProtectedRoute
exact
path="/path1"
name="path1"
location="/path1"
renderToaster={this.renderToaster}
component={MyForm}
/>
<ProtectedRoute
exact
path="/path2"
name="path2"
location="/path2"
component={params => <MyForm {...params} renderToaster={this.renderToaster} />}
/>
</Switch>
<Toaster
position={Position.BOTTOM}
ref={element => {
this.toaster = element;
}}
/>
</React.Fragment>
}
My ProtectedRoute class:
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { AuthContext } from '../providers/AuthProvider';
class ProtectedRoute extends Component {
state = {};
render() {
const { component, ...rest } = this.props;
const Component = component;
return (
<AuthContext>
{({ user }) => {
return user ? (
<Route render={params => <Component {...params} {...rest} />} />
) : (
<Redirect to="/" />
);
}}
</AuthContext>
);
}
}
export default ProtectedRoute;
And on my last class (MyForm passed to the protected Route) i call my renderToaster Method like this:
/**
* Component did Mount
*/
componentDidMount() {
this.props.renderToaster(Intent.PRIMARY, 'helloo');
}
You either need to bind renderToaster in the class constructor:
constructor(){
this.renderToaser = this.renderToaster.bind(this);
}
or declare renderToaser as an ES7 class property.
renderToaster = (intent, message) => {
this.toaster.show({
intent: intent,
message: message
});
}
The problem is this in renderToaster isn't pointing where you think it is when the method is passed to the child component. If you use either of these methods, then this will refer back to the class.
See the official docs for more detail: https://reactjs.org/docs/handling-events.html

Can't navigate to other component

i've got 2 components.
They are each on their own path.
I have a <Link> component in the first component, and when i click that, the second component is supposed to be rendered.
The url after the #, changes fine, but nothing happens in the ui.
This is the main component:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
ReactDOM.render(
<HashRouter>
<IntegrationApp />
</HashRouter>
, document.getElementById('integrationsOverviewContainer'));
The <Link> is a custom component.
It looks like this:
export default class LinkForRouting extends React.PureComponent {
render() {
let cssClass = this.props.isButton ? ' btn ' : '';
cssClass += this.props.isPrimary ? ' btn-primary ' : '';
cssClass += this.props.customCssClass ? this.props.customCssClass : '';
const clickEvent = this.props.handleClick ? () => this.props.handleClick() : ()=> function () { return null };
return (
<Link to={this.props.path} replace={true} className={cssClass} onClick={clickEvent}>
{this.props.children}
</Link>
);
}
}
The <Link> component is used inside the <IntegrationsOverview> component, like this:
<LinkForRouting path={`/details/${integration.encryptedId}/${integration.integrationType}`} isButton={false} isPrimary={false} > Vis </LinkForRouting>
If i click the link, and then hit F5 afterwards, then the <DetailsOverview> component renders fine, but if i just click the link, then the URL just changes, and nothing happens.
Any idea of how to trigger the <DetailsOverview> component, to trigger when i click on the <Link> component?
By reading the code posted above all of your components are extending PureComponent.
As React docs says:
React.PureComponent’s shouldComponentUpdate() only shallowly compares the objects. If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.Furthermore, React.PureComponent’s shouldComponentUpdate() skips prop updates for the whole component subtree. Make sure all the children components are also “pure”.
In your case:
export default class IntegrationApp extends React.PureComponent {
render() {
return (
<>
<Route path="/details/:encryptedId/:integrationType/" component={DetailsOverview} />
<Route exact path="/:integrationSource?/" component={IntegrationsOverview} />
</>
);
}
}
The IntegrationApp Component it is not expecting simple props and state also you should use a Switch instead of Fragment to wrap your Routes, then it becomes:
import React from "react";
import {
Switch,
Route
} from "react-router-dom";
export default class IntegrationApp extends React.Component {
render() {
return ( <
Switch >
<
Route path = "/details/:encryptedId/:integrationType/"
component = {
DetailsOverview
}
/> <
Route exact path = "/:integrationSource?/"
component = {
IntegrationsOverview
}
/> <
/Switch>
);
}
}
I wrote a simple example on: https://codesandbox.io/s/5vk79jlrn4
Hope it can help you.
The problem has been solved!
My react solution, was part of a much larger, and older solution.
This solution also had some references to angular, solution wide.
I made those references, specific to the views that need them, and then routing in react started working!
Lesson learned, check included javascript.

React Component Mounting Twice

Inside a small portion of my React/Redux/ReactRouterV4 application, I have the following component hierarchy,
- Exhibit (Parent)
-- ExhibitOne
-- ExhibitTwo
-- ExhibitThree
Within the children of Exhibit, there are about 6 different possible routes that can be rendered as well. Don't worry, I will explain with some code.
Here is my Parent Exhibit Component:
export class Exhibit extends Component {
render() {
const { match, backgroundImage } = this.props
return (
<div className="exhibit">
<Header />
<SecondaryHeader />
<div className="journey"
style={{
color: 'white',
backgroundImage: `url(${backgroundImage})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center-center'
}}>
<Switch>
<Route path={`${match.url}/exhibit-one`} component={ExhibitOne} />
<Route path={`${match.url}/exhibit-two`} component={ExhibitTwo} />
<Route path={`${match.url}/exhibit-three`} component={ExhibitThree} />
<Redirect to="/" />
</Switch>
</div>
</div>
)
}
}
Basically, all its does for its job is to display one of the exhibits subcomponents, and set a background image.
Here is one of the subcomponents, ExhibitOne:
export default class ExhibitOne extends Component {
constructor(props) {
super(props)
}
render() {
const { match } = this.props
return (
<div className="exhibit-one">
<Switch>
<Route path={`${match.url}/wall-one`} component={ExhibitHOC(WallOne)} />
<Route path={`${match.url}/wall-two`} component={ExhibitHOC(WallTwo)} />
<Route path={`${match.url}/wall-three`} component={ExhibitHOC(WallThree)} />
<Route path={`${match.url}/wall-four`} component={ExhibitHOC(WallFour)} />
<Route path={`${match.url}/wall-five`} component={ExhibitHOC(WallFive)} />
<Route path={`${match.url}/wall-six`} component={ExhibitHOC(WallSix)} />
</Switch>
</div>
)
}
}
In order to cut down on typing, I decided to wrap the components in a Higher Order Component, whose
purpose is to dispatch an action that will set the proper background image on the top level Exhibit parent component.
This is the Higher Order Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions/wall-background-image'
export default function(ComposedComponent) {
class ExhibitHoc extends Component {
componentDidMount = () => this.props.setBackgroundImage(`./img/exhibit-one/${this.getWall()}/bg.jpg`)
getWall = () => {
// this part isnt important. it is a function that determines what wall I am on, in order to set
// the proper image.
}
render() {
return <ComposedComponent />
}
}
return connect(null, actions)(ExhibitHoc);
}
On initial load of ExhibitOne, I can see that the setBackgroundImage action creator executes twice by looking
at Redux Logger in the console. My initial inclination to use componentDidMount was because I thought using it
would limit the action creator to execute only once. Here is a screenshot of the log:
I think I might be misunderstanding how Higher Order Components work, or maybe its some type of React Router V4 thing?
Anyways, any help would be greatly appreciated as to why this executes twice.
Here in 2020, this was being caused by <React.StrictMode> component that was wrapped around the <App /> in new versions of Create React App. Removing the offending component from index.js fixed the double mount problem for all of my components. This was by design, but it was annoying and misleading to see console.log() twice for everything.
For Next.js, in next.config.js, set reactStrictMode:false.
The problem is that the component prop here is a function application, which yields a new class on each render. This will cause the previous component to unmount and the new one to mount (see the docs for react-router for more information). Normally you would use the render prop to handle this, but this won't work with higher-order components, as any component that is created with a HOC application during rendering will get remounted during React's reconciliation anyway.
A simple solution is to create your components outside the ExhibitOne class, e.g.:
const ExhibitWallOne = ExhibitHOC(WallOne);
const ExhibitWallTwo = ExhibitHOC(WallTwo);
..
export default class ExhibitOne extends Component {
..
<Route path={`${match.url}/wall-one`} component={ExhibitWallOne} />
<Route path={`${match.url}/wall-two`} component={ExhibitWallTwo} />
..
}
Alternatively, depending on what the wrapper does, it might be possible to declare it as a normal component that renders {this.props.children} instead of the parameter <ComposedComponent/>, and wrap the components in each Route:
<Route path={`${match.url}/wall-one`}
render={(props) => <Wrap><WallOne {...props}/></Wrap>}
/>
Note that you'll need to use render instead of component to prevent remounting. If the components don't use routing props, you could even remove {...props}.
If you use 'Hidden Material UI React', it mounts your component every time you call it. For example, I wrote the below one:
<Hidden mdDown implementation="css">
<Container component="main" maxWidth="sm">
{content}
</Container>
</Hidden>
<Hidden smUp implementation="css">
{content}
</Hidden>
It invokes both contents in both hidden components. it took me a lot of time.

Prompt is made when navigating within same route

For the use case when a page has an internal route, the prompt triggers even when navigating to an internal route where a prompt might not be necessary, see code example. Is there a way to disable the prompt when navigating to known safe routes?
import React, {Component} from 'react';
import {BrowserRouter, Route, Switch, Link, Prompt} from 'react-router-dom';
export default class App extends Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route component={About} path={"/about"}/>
<Route component={Home} path={"/"}/>
</Switch>
</BrowserRouter>
);
}
}
class Home extends Component {
constructor(props) {
super(props);
this.state = {input: 'hello?'}
}
render() {
return (
<div>
<h1>Home</h1>
<input value={this.state.input}
onChange={(e) => this.setState({input: e.target.value})}/><br />
<Link to={"/info"}>More Info</Link><br />
<Link to={"/about"}>About Page</Link><br />
{/*Triggers even when going to /info which is unnecessary*/}
<Prompt message="Move away?" when={this.state.input !== 'hello?'}/>
<Route path={"/info"} component={Info}/>
</div>
);
}
}
class About extends Component {
render() {
return (
<h1>About page</h1>
);
}
}
class Info extends Component {
render() {
return (
<p>Here be some more info</p>
);
}
}
In the example above, About is a different page and so should trigger when the input has changed, which it does correctly. But the /info route is an internal route for Home, so the prompt is unnecessary, the internal state of Home is preserved after navigation so nothing is lost.
The actual use case here is for a modal to be shown when the route is active, but that is mostly CSS stuff so I excluded it from the example.
I think a callback function as a message prop is what are you looking for. You can try to give a callback function to Prompt's message prop. That callback will be called with an object as an argument which will have all the information about the next route.
This is what documentation says about it:
message: func
Will be called with the next location and action the
user is attempting to navigate to. Return a string to show a prompt to
the user or true to allow the transition.
The object has a pathname attribute which is the next path by checking it you can figure out if the path is safe. Here is what I'm talking about:
<Prompt message={(params) =>
params.pathname == '/about' ? "Move away?" : true } />
And here is the pen with working code which I've created from your examples.
Hope it helps.

Resources