Can I create 2 components which share a state? - reactjs

In my React app, I have a layout file, and I want to be able to pass 2 different components into it. One component is to be shown in Area 1, and another is to be shown in Area 2. Both components need to share information with each other.
So, my layout is:
const SplitLayout = (Area1Content, Area2Content) => {
return (
<div className="area1">
<Area1Content />
</div>
<div className="area2">
<Area2Content />
</div>
);
}
export default SplitLayout;
In my App.js I have:
const App = () => (
<Router>
<Switch>
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
Area1Content={HomeContent}
Area2Content={SidebarContent} />}/>
</Switch>
</Router>
);
export default App;
This works fine; I can put HomeContent and SidebarContent into a file and export both of them, and they are shown correctly.
However, I want to be able to pass information from one to the other, so, for instance, SidebarContent has a list of names; when I click on a name in the list, I want that person's details to be shown in HomeContent (so in the HomeContent component I can have a state variable called currentPerson, and when a name is clicked in SidebarContent, the value of currentPerson should be changed).
Is there a way to achieve this?
I have several pages with similar layouts, so what I'm hoping is that I can have, eg, a HomeComponent.js file which has HomeContent and SidebarContent, and then another component called, say, SecondComponent.js which has SecondContent and SecondSidebar, so I can just add a new Route to App, something like:
<Route exact path={ROUTES.SECOND}
render={(props) =>
<SplitLayout {...props}
Area1Content={SecondContent}
Area2Content={SecondSidebar} />}/>
so it will render the same layout but with different components. I know I could lift the state up to the top level, but there could potentially be several different component pairs, each needing to pass info, and I think it would get messy to manage all of them at the App level. Is there a better way?
EDIT: I think what I want to do is something like this:
In App.js my route would be something like:
<Route exact path={ROUTES.HOME}
render={(props) =>
<SplitLayout {...props}
PageContent={WrapperComponent} />}/>
Then in the SplitLayout file I'd have something like:
const SplitLayout = (WrapperComponent) => {
return (
<div className="area1">
<WrapperComponent.Area1Content/>
</div>
<div className="area2">
<WrapperComponent.Area2Content/>
</div>
);
}
And WrapperComponent would be something like:
const WrapperComponent = () => {
const [myStateVariable, setMyState] = useState("xyz")
const Area1Content = () => {
return (<div>{myStateVariable}</div>);
}
const Area2Content = () => {
return (<div onClick={setMyState("abc")}>Something else</div>);
}
}
export default WrapperComponent;
Is there a way to do something like that?

You can put the shared state in the parent component, and pass it down as props.

Related

Passing and spreading props in React Router Dom v 6

I have the below working in RRD v 5.3.0 but cant get an alternative version working in v6. Ive tried using 'element' but it doesn't seem to work
<BrowserRouter>
<Route render={props => (
<>
<Header {...props}/>
<Routes/>
<Footer/>
</>
)}/>
</BrowserRouter>
Version 6 no longer supports the render function style. Instead the preferred approach is to use the use hooks (eg, useLocation, useParams) in the child component to access whatever values are needed.
For example, if header was previously expecting to get location and match via its props:
const Header = ({ match, location }) => {
// ...
}
It would now do:
const Header = () => {
const location = useLocation();
const match = useMatch();
// ...
}
If header can't be modified for some reason, then you could make a component that uses these hooks, and then passes the values down to the header as props:
const Example = () => {
const location = useLocation();
const match = useMatch();
return (
<>
<Header location={location} match={match}>
<Routes/>
<Footer/>
<>
)
}
// used like:
<Route>
<Example/>
</Route>
For a guide on updating from version 5 to 6, see this page
For information about why they made this change, see this article

How to pass props to child component based on dynamic route

I'm trying to make an ecommerce website with react and react-router-dom. I have different components: ProductsList, Product, Details (about single product).
I am using Context API in a separate file. My App.js file looks like this:
...imports
const products = useContext(ProductContext)
const App = () => {
...
<Route path="/products/:id">
<Details products={products} />
</Route>
}
But I want to send details only about the product whose id matches the dynamic route id, not the whole array of products. But how do I extract that id? I know I can do it inside the Details.jsx file by using useParams(). But how do I do that from inside App.js?
I just noticed there's a similar quesiton. But I want my Details component in its own file and not inside App.
You can try following:-
...imports
const products = useContext(ProductContext)
const App = () => {
...
<Route path="/products/:id" render={(props) => {
const id = props.match.params.id;
const productData = products.find(product => product.id === id);
return <Details {...props} product={productData} />
}}
</Route>
}

React prevent remounting components passed from props

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.

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