React prevent remounting components passed from props - reactjs

When using React with React Router I run in some mounting issues.
This might not even be a problem with React Router itself.
I want to pass some additional data along with the child routes.
This seems to be working, however the changes on the main page trigger grandchildren to be remounted every time the state is changed.
Why is this and why doe this only happen to grandchildren an not just the children ?
Code example:
import React, { useEffect, useState } from 'react';
import { Route, Switch, BrowserRouter as Router, Redirect } from 'react-router-dom';
const MainPage = ({ ChildRoutes }) => {
const [foo, setFoo] = useState(0);
const [data, setData] = useState(0);
const incrementFoo = () => setFoo(prev => prev + 1);
useEffect(() =>{
console.log("mount main")
},[]);
useEffect(() =>{
setData(foo * 2)
},[foo]);
return (
<div>
<h1>Main Page</h1>
<p>data: {data}</p>
<button onClick={incrementFoo}>Increment foo {foo}</button>
<ChildRoutes foo={foo} />
</div>
);
};
const SecondPage = ({ ChildRoutes, foo }) => {
const [bar, setBar] = useState(0);
const incrementBar = () => setBar(prev => prev + 1);
useEffect(() =>{
console.log("mount second")
},[]);
return (
<div>
<h2>Second Page</h2>
<button onClick={incrementBar}>Increment bar</button>
<ChildRoutes foo={foo} bar={bar} />
</div>
);
};
const ThirdPage = ({ foo, bar }) => {
useEffect(() =>{
console.log("mount third")
},[]);
return (
<div>
<h3>Third Page</h3>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
</div>
);
};
const routingConfig = [{
path: '/main',
component: MainPage,
routes: [
{
path: '/main/second',
component: SecondPage,
routes: [
{
path: '/main/second/third',
component: ThirdPage
},
]
}
]
}];
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
</Route>
);
})}
</Switch>
);
};
export const App = () => {
return(
<Router>
<Routing routes={routingConfig}/>
<Route exact path="/">
<Redirect to="/main/second/third" />
</Route>
</Router>
)
};
export default App;
Every individual state change in the MainPage causes ThirdPage to be remounted.
I couldn't create a snippet with StackOverflow because of the React Router. So here is a codesandbox with the exact same code: https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js
Expected behavior is for every page to only trigger the mounting once.
I know I can probably fix this by using Redux or React.Context, but for now I would like to know what causes this behavior and if it can be avoided.
==========================
Update:
With React.Context it is working, but I am wondering if this can be done without it?
Working piece:
const ChildRouteContext = React.createContext();
const ChildRoutesWrapper = props => {
return (
<ChildRouteContext.Consumer>
{ routes => <Routing routes={routes} {...props} /> }
</ChildRouteContext.Consumer>
);
}
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<ChildRouteContext.Provider value={routes}>
<Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
</ChildRouteContext.Provider>
</Route>
);
})}
</Switch>
);
};

To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.
React component is either a class-based or functional component. You could think of it as a function that will accept some props and
eventually return a React element. And you should create a React component only once.
React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide
the syntax for creating a React element by its React component:
<Component someProps={...} />
At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.
Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:
Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props
That's a brief of the theory, let's get into pratice.
I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.
This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):
The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.
To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.
<Component
{...rest}
renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0
Reference:
React Components, Elements, and Instances
Reconciliation
Render Props

The culprit is this line:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:
let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>
a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.
The solution is to create this functional component once, in advance, outside your render method, like so:
const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />
... and then pass this single functional component:
<Component {...rest} ChildRoutes={ChildRoutesWrapper} />

Your components are remounting every time because you're using the component prop.
Quoting from the docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
The solution you probably need in your case is to edit your Routing component to use render instead of children.

Related

React router 6 never unmount component when URL parameter changes

We have a problem with the react router v6. When the URL parameters change, it is already using the mount component. Never unmount and mount the component.
Produced code example - if switching between link 1 and link 2(url parameter (id) changes), the Link component never unmount and mount:
https://stackblitz.com/edit/github-agqlf5-gohmbu?file=src/routes/link.jsx
So how do we get it to be unmount and mount component when the url parameter changes?
This is by-design. If you look at the Route rendering the Invoices component
<Route path="link/:id" element={<Invoices />} />
There is nothing here to cause (a) the Route to remount, and (b) the routed component Invoices to remount. If the route path updates it will only trigger a rerender of the routed component, only values in the routing context have changed really. Think of this more like an update to props triggering a component to rerender, not remount.
If the routed component needs to "listen" to and respond to route param changes then it should use a useEffect hook to issue any side-effects.
Example:
import { Outlet, useParams } from 'react-router-dom';
export default function Invoices() {
const { id } = useParams();
React.useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
React.useEffect(() => {
console.log("Route parameter changed", { id });
}, [id]);
return (
<div style={{ display: 'flex', paddingTop: '1rem' }}>
<nav>Invoice ID: {id}</nav>
<Outlet />
</div>
);
}
If you really want to remount the routed component then create a wrapper component that reads the id route path param and sets a React key on the routed component. The InvoicesWrapper will remain mounted but when the id route param updates the Invoices component will remount since it has a new React key.
Example:
const InvoicesWrapper = () => {
const { id } = useParams();
return <Invoices key={id} />
};
...
...
<Route path="link/:id" element={<InvoicesWrapper />} />
...

How do components re-render in redux using hooks

I am trying to conceptualize redux and its working, and after some testing, I have noticed a thing. I would like to quote this example
lets say, I have a single reducer (a boolean variable). based on that variable, the following code happens.
reducer
const initState = { isLoggedIn: false };
const isLoggedInReducer = (state = initState, action) => {
switch (action.type) {
case "LOG_IN":
return { ...state,isLoggedIn: true };
case "LOG_OUT":
return { ...state,isLoggedIn: false };
default:
return state;
}
};
export default isLoggedInReducer;
action
export const logIn = () => {
return {
type:'LOG_IN'
}
}
export const logOut = () => {
return {
type:'LOG_OUT'
}
}
index.js
<Provider store={createStore(isLoggedInReducer)}>
<Router>
<Switch>
<Route path="/" exact>
<AppScreen />
</Route>
<Route path="/auth">
<AuthScreen />
</Route>
<Route path="*">
<NotFoundScreen />
</Route>
</Switch>
</Router>
</Provider>
so, my app firstly directs the user to a component called "mainScreen" , which is as follows
const AppScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
if (isLoggedIn)
return (
<>
<button onClick={() => dispatch(logOut())}>unauthenticate</button>
<NavBar />
<Content />
<BottomBar />
</>
);
else{
return (
<>
<Redirect to="/auth" push />
</>
);
}
};
so if the reducer state has value TRUE , my navbar and stuff is shown, else the user is redirected to the "authScreen" , which is as
const AuthScreen = () => {
let isLoggedIn = useSelector((state) => state.isLoggedIn);
const dispatch = useDispatch();
return isLoggedIn ? (
<>
<Redirect to="/" push />
</>
) : (
<>
<h1> auth is {isLoggedIn?"true":"false"}</h1>
<button onClick={() => dispatch(logIn())}>authenticate</button>
</>
);
};
This creates a setup where "authScreen" can toggle the reducer to TRUE and it re-renders, and finds that reducer is TRUE, so it renders the "mainScreen". Vice versa for "MainScreen"
Now, what components actually re-render ? If I place my authenticate button in the "navbar" instead as a sibling to "navbar" , will it re-render the "navbar" or the "mainScreen" ?
How does redux calculate what component to re-render when a peice of state changes ? How does the useSelector fit in, when I did not even use "connect".
Using hooks with redux made it very confusing. I am sorry if my explanation is hard to understand. The code actually works, I just don't know how.
Any piece of information is appreciated!
Using Redux with a UI always follows the same basic steps:
Render components using initial state
Call store.subscribe() to be notified when actions are dispatched
Call store.getState() to read the latest data
Diff old and new values needed by this component to see if anything actually changed. If not, the component doesn't need to do anything
Update UI with the latest data
React-Redux does that work for you internally.
So, useSelector decides whether a component should re-render based on whatever data you return in your selector functions:
https://redux.js.org/tutorials/fundamentals/part-5-ui-react#reading-state-from-the-store-with-useselector
If the selector return value changes after an action was dispatched, useSelector forces that component to re-render. From there, normal React rendering behavior kicks in, and all child components are re-rendered by default.
Please read my post The History and Implementation of React-Redux and talk A Deep Dive into React-Redux for details on how React-Redux actually implements this behavior.

React constructor(), componentDidmount with props variable

Where I can call the constructor() and componentDidmount event with below code:
export const Home = props => (props.isAuthenticated ? (
<DashBoard {...props} />
) : (<Marketing {...props} />));
What is the meaning of the above code and how it's work?
This is a functional component, correctly formatted is probably a little easier to read:
export const Home = props => (
props.isAuthenticated ? (
<DashBoard {...props} /> // if authenticated return and render Dashboard
) : (
<Marketing {...props} /> // else return and render Marketing
)
);
In functional components use the useEffect hook with an empty dependency array to achieve the equivalent of a class-based component's componentDidMount. Hooks are called on mount and whenever a variable in its dependency array are updated.
effect hook
export const Home = props => {
useEffect(() => console.log("I just mounted!", []); // empty so called once when the component is mounted
return (
props.isAuthenticated ? (
<DashBoard {...props} /> // is authenticated return and render Dashboard
) : (
<Marketing {...props} /> // else return and render Marketing
)
);
};
You cannot use react lifecycle hooks in a functional component. Refer to react documentation below for usage of lifecycle hooks, and to convert functional components to class components.
https://reactjs.org/docs/state-and-lifecycle.html
export default class Home extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {}
render() {
const { isAuthenticated } = this.props;
return (
<>
{isAuthenticated ? <DashBoard {...this.props} /> : <Marketing {...this.props} />}
</>
);
}
}
export const Home = props => (props.isAuthenticated ? (
<DashBoard {...props} />
) : (<Marketing {...props} />));
Details
So the above code is a functional component, currently functional components can handle all the lifecycle methods that we use in class based components
So prev, before 16.8 of reactjs we can have state and life cycle methods in a functional components, It was only used for rendering the elements like as a presentational components. So at a point for complex applications we need to convert the functional components to class based components to handle a single state change
So this made the evolution of hooks, you can read more on the official docs of react js
So comming to your case if you need to call the method in componentDidMount, you can call as shown below
useEffect(() => {
// your logic same as componentDidMount in class based components
}, [])
So the second argument is the dependencies for the useEffect to trigger
if you pass it as like this it will call every time
useEffect(() => {})
If you pass it as like this it will call whenever the passed variable changes from props or state
useEffect(() => {}, [data, userName])
I hope this will give a better understanding of the problem

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

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')
// ..
}
// ...
}

React Flux - Pass props to react router

I am using the React flux architecture as shown in this example from facebook. https://github.com/facebook/flux/tree/master/examples/flux-todomvc. I am also using react-router in my app. I am following the switch example https://reacttraining.com/react-router/web/example/modal-gallery as I need to render the same screen in a different context. So from the example if I click on a color I would load a screen in that color. The only difference is that I want to load the list of colors dynamically from an Api.
My container is as below
AppContainer.js
function getStores() {
return [
ColorStore
];
}
function getState() {
return {
colors: ColorStore.getState()
};
}
export default Container.createFunctional(AppView, getStores, getState);
AppView.js
const AppView = (props) => {
return (
<Router>
<Route component={RouteSwitch} {...props} />
</Router>
);
}
But I am unable to access custom props in the Route component. I only get history, location & match. How do I do this?
Pass the values like this:
<Route component={(props) => <RouteSwitch {...props} />} />

Resources