React render single instance of mounted component into different children - reactjs

I am building a sequential step-based wizard flow that looks something like this:
export default function App() {
return (
<Wizard footer={<Footer />}>
<Step1 />
<Step2 />
</Wizard>
);
}
the <Step /> components are mounted/unmounted based on the step the user is currently on.
Problem:
I have an expensive component, <SomeExpensiveComponent />. It is very computationally expensive to mount, but I need to render it in multiple <Step />s. I want to avoid having it mount multiple times in my app:
// ❌ BAD - each step is mounting its own, new instance of SomeExpensiveComponent
const Step1 = () => {
return (
<Step.Wrapper>
<Step.Preview>
<SomeExpensiveComponent />
</Step.Preview>
<Step.Content>Step1</Step.Content>
</Step.Wrapper>
);
};
const Step2 = () => {
return (
<Step.Wrapper>
<Step.Preview>
<SomeExpensiveComponent />
</Step.Preview>
<Step.Content>Step2</Step.Content>
</Step.Wrapper>
);
};
I need to render this component inside of various <Step />s, but only want to mount the component one time, and share the single instance between steps. Something like this:
export default function App() {
return (
<>
{/* 👀 mount expensive component once, here */}
<SomeExpensiveComponent />
<Wizard footer={<Footer />}>
<Step1 />
<Step2 />
</Wizard>
</>
);
}
const Step1 = () => {
return (
<Step.Wrapper>
<Step.Preview>
{/* ✅ ...and output that single instance into an "outlet" when this component is mounted */}
<OutletForExpensiveComponent />
</Step.Preview>
<Step.Content>Step1</Step.Content>
</Step.Wrapper>
);
};
const Step2 = () => {
return (
<Step.Wrapper>
<Step.Preview>
{/* ✅ ...and output that single instance into an "outlet" when this component is mounted */}
<OutletForExpensiveComponent />
</Step.Preview>
<Step.Content>Step2</Step.Content>
</Step.Wrapper>
);
};
The hypothetical "outlet" above would simply dictate different render targets for the single shared instance of SomeExpensiveComponent.
At first, it sounds like React Portals might be the solution to this problem:
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
However, it seems Portals are primarily focused on the opposite direction - getting a leaf node to render in a higher scope or different tree, not getting a trunk node to render output into a different leaf node.
Question:
Are portals the right way to accomplish the above? If so, how?
Is this accomplishable with React Context? If so, how?
Code / demo
You can see my current attempt (using React Context) here: https://codesandbox.io/s/quizzical-elgamal-wvhc3h?file=/src/App.tsx
Problems:
SomeExpensiveComponent renders > 1 time on app load.
SomeExpensiveComponent re-renders on navigate.
I tried this approach with react portal, which seems closer, but still get 2 renders instead of 1 on page load:
https://codesandbox.io/s/young-bash-iij752?file=/src/App.tsx

Related

How to get DOM elements within (any/all) nested components in React?

I have a wrapper component in my App as follows. I have logic in the wrapper component that gets the DOM nodes with props.children that have a specific data attribute.
When the DOM elements are direct children, this works fine. When they are children of any nested component they're not found. How can I iterate through the entire structure and get all instances of the DOM nodes by attr?
I'm new to react and sure this should be straightforward, however I've not been able to implement from a number of examples / SO answers. I've tried to implement useRef and useContext hooks but can't get this?
// App.js
<Wrapper>
<div data-elem></div> // Get's found
<Component {...pageProps} />
</Wrapper>
// index.js
const Page = () => {
return (
<>
<div data-elem></div> // Not found
</>
);
}
// Wrapper.js ( simplified version )
export default function Wrapper(props) {
const detectedElements = props.children.filter((item) => item.props['data-elem'] === true);
console.log(detectedElements.length)
return (
<div className="wrapper">
{children}
</div>
)
}
Ultimately I managed to achieve this simply by using querySelectorAll within the useEffect hook. I'm not sure if this is the 'correct' way to do it, but is definitely the most straightforward of the the variations I tried, and seems to do the job, regardless of where the corresponding elements are.
useEffect(() => {
const detectedElements = document.querySelectorAll('[data-elem]')
}, []);

React component seems to rerender because of child components (but shouldn't)

I have a page with a tabbed view. If tab 1 is active it should show component A, otherwise component B.
Based on which page/route I come from before, it should default to component B. Both the components don't need props (as they handle data/state internally).
Problem: The parent component renders three times. Because of this, I loose the state value from useLocation and can't display the second tab. Simplified parent component code:
function ContractOverview() {
const location = useLocation();
console.log(location);
const showBillingCycleTab = location.state?.selectedTab === 'billing-cycles';
return (
<Container>
<h1>
<Trans i18nKey="navigation.billing" />
</h1>
{showBillingCycleTab ? <BillingCycleTable /> : <ContractTable />} // if I do it like this, ContractOverview renders three times (and I loose location state)
{showBillingCycleTab ? <p>component A</p> : <p>component B</p>} // if I do it like this, ContractOverview renders only once and the location state is correct.
</Container>
);
}
The route is set up like this:
<Route path="/contracts" component={ContractOverview} exact />
Screenshot of the location logs:
Usually I'd expect the parent component to only render once as there are no props that could change nor component state that could do something. What am I doing wrong?

React Router: navigate to /compose/tweet but keep orthogonal previous route (like: /notifications) mounted

I am seeking to recreate a pattern with React Router. It is best described by the Twitter example: as you hit the Tweet button, the browser navigates to /compose/tweet, mounting the composer component. However, and that's the key, the previous route (/home, /explore, /notifications, /messages) is kept mounted despite the route change. How do you do that?
This could be called bidimensional routing: the /compose/tweet route is kept orthogonal with respect to the other routes that render the main view. The other routes are hidden (i.e., not in the address bar any longer) upon navigating to /compose/tweet, thereby rendering two independent routes (say, /notifications and /compose/tweet) at once.
My actual example: I need to show a user settings menu (/user/menu) as a large sidebar, but I do not want that to change whether the user was navigating / (the homepage), /faq, /contact, etc. Based on my current understanding of React Router, as soon as you hit /user/menu, any other route (take /faq as an example) would be unmounted based on route match.
Caching the previous route (e.g., Redux, which I'm using extensively already) does not seem feasible, since, even though I would be able to redirect the user to the previous route upon exiting /user/menu, React would still be unmounting components, in fact showing the homepage in the background until the user exits /user/menu & gets redirected to where they were at, which is not the intended behaviour. I would want the rest of the page to stay there with the rendered components, just the way Twitter does.
Am I overlooking anything? Is this an easy pattern and I am missing something?
Note: it's a SSR isomorphic app, but I guess/hope that won't change things.
Despite Adam Abramov's suggestion to keep React Router as the source of truth for whatever can be passed as route, and avoid deep integrations between Router and Redux, I found myself having to use Redux as the main source of truth in this (important) use case. I still wanted to have Route components for SSR and SEO purposes.
So, I created my own MultidimensionalSwitch and MultidimensionalRoute components to solve this use case. If a MultidimensionalSwitch is mounted, it will render the components at their subroutes, but if none of them is matched, it will render them based on another dimension, which is provided by Redux at an additional alt property of the corresponding MultidimensionalRoute.
Here below is some code, feel free to answer/ask should you need more info about it.
Main
class _Main extends Component {
render() {
const {menuOpen,selected} = this.props;
return (
<Fragment>
<Route exact path={exactRoutes.ROOT} component={() => <Redirect to={selected?exactRoutes[selected]:exactRoutes.HOME} />} />
<Header />
<Route path={nestedRoutes.AUTH+routeParams.LOGIN_SIGNUP.key} component={ScreenAuth} />
<Route path={exactRoutes.USER_MENU} component={ScreenUser} />
{!menuOpen?"":<ScreenMenu />}
<ScreenMain>
<MultidimensionalSwitch>
<MultidimensionalRoute path={exactRoutes.HOME} alt={selected===guestMenuOption.HOME} component={GuestHome} />
<MultidimensionalRoute path={exactRoutes.USER} alt={selected===guestMenuOption.USER} component={User} />
<MultidimensionalRoute path={exactRoutes.VISION} alt={selected===guestMenuOption.VISION} component={GuestVision} />
<MultidimensionalRoute path={exactRoutes.FAQ} alt={selected===guestMenuOption.FAQ} component={GuestFaq} />
<MultidimensionalRoute path={exactRoutes.INFOGRAPHICS} alt={selected===guestMenuOption.INFOGRAPHICS} component={GuestInfographics} />
<MultidimensionalRoute path={exactRoutes.BLOG} alt={selected===guestMenuOption.BLOG} component={GuestBlog} />
<MultidimensionalRoute path={exactRoutes.CONTACT} alt={selected===guestMenuOption.CONTACT} component={GuestContact} />
</MultidimensionalSwitch>
<Footer />
</ScreenMain>
<Flare />
</Fragment>
);
};
}
const mapStateToProps = state => ({
menuOpen: state.client.guest.menuOpen,
selected: state.client.guest.guestMenuOption,
});
const Main = withRouter(connect(mapStateToProps,{})(_Main));
export default Main;
MultidimensionalSwitch
class _MultidimensionalSwitch extends Component {
render() {
return (
<Fragment>
<Switch>
{this.props.children}
{this.props.children.map(child => !child.props.alt?"":<Route path={nestedRoutes.ROOT} component={child.props.component} />)}
</Switch>
</Fragment>
);
}
}
const mapStateToProps = state => ({
selected: state.client.guest.guestMenuOption,
});
const MultidimensionalSwitch = withRouter(connect(mapStateToProps,{})(_MultidimensionalSwitch));
export default MultidimensionalSwitch;
MultidimensionalRoute
class _MultidimensionalRoute extends Component {
render() {
const {path,component} = this.props;
return (
<Fragment>
<Route exact path={path} component={component} />
</Fragment>
);
}
}
const mapStateToProps = (state,ownProps) => ({
path: ownProps.path,
component: ownProps.component,
});
const MultidimensionalRoute = withRouter(connect(mapStateToProps,{})(_MultidimensionalRoute));
export default MultidimensionalRoute;

When react perform componentDidMount and componentWillUnmount

I played with React for several years, still confused with mount/unmount mechanism in some case.
Since mount/unmount is the place to perform side effect, I do not want them to be invoked randomly. So I need to figure out how they work. As far as I can understand currently, when the virtual dom do not present in real dom, it tend to be unmounted. However, it seems not the whole story, and I can not reason it about
function TestMount(props) {
useEffect(() => {
console.log("componentDidMount", props.name);
return () => {
console.log("componentWillUnount", props.name);
};
}, []);
return <h1>Test content {" " + JSON.stringify(props.name)}</h1>;
}
function Update({ click }) {
return <button onClick={click}>Update</button>;
}
function App() {
const [count, setCount] = useState(0);
const Component = name => <TestMount name={name} />;
return (
<div className="App">
<h1>{count}</h1>
<Component name="one" />
{Component("two")}
<Update click={() => setCount(x => x + 1)} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Component One is remount overtime the app render while Component two not?Why this happen?
Component is a new function each time App is rendered, so <Component name="one" /> is remounted each time too, they are considered different components.
The result of Component("two") call is <TestMount name={"two"} />, TestMount stays the same each time App is rendered, so it's not remounted.
Component is invalid component for what it's used for, to pass name string as name prop to TestMount component because name parameter is not a string but props object when Component is used like <Component name="one" />. name => <TestMount name={name} /> is render function, it's preferable to name it accordingly like renderTestMount for clarity because components aren't supposed to be called directly like Component("two").
In case a function is supposed be used as component or render function interchangeably, the signature should be changed to ({ name }) => <TestMount name={name} />.
The expected behaviour could be achieved for <Component name="one" /> by memoizing Component:
const Component = useCallback(({ name }) => <TestMount name={name} />, []);
But since Component doesn't depend on App scope, a correct way is to define it outside:
const Component = ({ name }) => <TestMount name={name} />;
function App() {...}
For instance, this is the reason React Router Route has separate component and render props for a component and render function. This allows to prevent unnecessary remounts for route components that need to be defined dynamically in current scope.
The key to such issue is the difference between the React Component and React element, put shortly React is smart with element not Component
Component vs element
Component is the template used to create element using <> operation. In my prospective, <> is pretty much like new operator in OOP world.
How React perform update between renders
Every time the render method(or functional component) is invoked. The new element is created using <>, however, React is smart enough to tell the element created between renders are actually the same one, i.e. it had been created before and can be reused as long as the element is created by the same Component
How about different Component
However when the identity of the Component using to generate element changes(Even if the Components look same), React believes something new come though, so it remove(unmount) the previous element and add(mount) the new one. Thus componentDidMount or componentWillUnmount is invoked.
How is confusing
Think we got a Component and when we generate element using <Component /> react can tell the same elements because they are generated by the same Component
However, HOCComponent=()=><Component />; element= <HOCComponent />, every time element is generated, it used a different Component. it is actually a HOC constructed dynamically. Because the HOC is created dynamically inside render function, it can be confusing on the first glance.
Is that true
I never found any offical document about the idea above.However the code below is enough to prove
function TestMount(props) {
useEffect(() => {
console.log("componentDidMount", props.name);
return () => {
console.log("componentWillUnount", props.name);
};
}, []);
return <h1>Test content {" " + JSON.stringify(props.name)}</h1>;
}
function Update({ click }) {
return <button onClick={click}>Update</button>;
}
let _Component;
function cacheComponent(C) {
if (C && !_Component) {
_Component = C;
}
return _Component || null;
}
const CacheComponent2 = once(({ name }) => <TestMount name={name} />, []);
function App() {
const [count, setCount] = useState(0);
// can be used as a HOC of TestMount or a plain function returnnung a react element
const Component = name => <TestMount name={name} />;
const CacheComponent1 = cacheComponent(Component);
const CacheComponent3 = useCallback(
({ name }) => <TestMount name={name} />,
[]
);
return (
<div className="App">
<h1>{count}</h1>
{/* used as HOC */}
<Component name="one" />
{/* used as function returnning the element */}
{Component("two")}
<CacheComponent1 name="three" />
<CacheComponent2 name="four" />
<CacheComponent3 name="five" />
<Update click={() => setCount(x => x + 1)} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Also the code above provide three different ways to avoid the undesired mount/unmount. All the solutions are cache the identity of the HOC somehow

Set state property from URL using react-router

I have a container component with a modal in it that is opened and closed based on a state property.
I want to control this via the URL, i.e. I want to have
/projects - the modal is NOT open
/projects/add - the modal IS open
As well as being able to link directly to it, I want the URL to change when I click on links within the main container to open the modal.
Can someone explain how I could do this, or point me in the right direction of a good tutorial?
NOTE: This way is not perfect. Even more it's rather antipattern than pattern. The reason I publish it here is it works fine for me and I like the way I can add modals (I can add modal to any page and in general their components don't depends on the other app in any way. And I keep nice url's instead of ugly ?modal=login-form). But be ready to get problems before you find everything working. Think twice!
Let's consider you want following url's:
/users to show <Users /> component
/users/login to show <Users /> component and <Login /> modal over it
You want Login to not depend on Users in anyway, say adding login modal to other pages without pain.
Let's consider you have kinda root component which stands on top of other components. For example Users render may look something like this:
render() {
return (
<Layout>
<UsersList />
</Layout>
);
}
And Layout's render may look something like this:
render() {
return (
<div className="page-wrapper">
<Header />
<Content>
{this.props.children}
</Content>
<Footer />
</div>
);
}
The trick is to force modal's injection to <Layout /> every time we need it.
The most simple approach is to use flux for it. I'm using redux and have ui reducer for such page meta-information, you can create ui store if you use other flux implementation. Anyway, final goal is to render modal if <Layout />'s state (or even better props) contains modal equal to some string representing modal name. Something like:
render() {
return (
<div className="page-wrapper">
<Header />
<Content>
{this.props.children}
</Content>
{this.props.modal ?
<Modal key={this.props.modal} /> :
null
}
<Footer />
</div>
);
}
<Modal /> returns modal component depends on given key (In case of our login-form key we want to receive <Login /> component).
Okay, let's go to router. Consider following code snippet.
const modal = (key) => {
return class extends React.Component {
static displayName = "ModalWrapper";
componentWillMount() {
// this is redux code that adds modal to ui store. Replace it with your's flux
store.dispatch(uiActions.setModal(key));
}
componentWillUnmount() {
store.dispatch(uiActions.unsetModal());
}
render() {
return (
<div className="Next">{this.props.children}</div>
);
}
}
};
...
<Route path="users" component={Next}>
<IndexRoute component={Users}>
<Route path="login" component={modal('login-form')}>
<IndexRoute component={Users} />
</Route>
</Route>
(Don't care about Next - I add it here for simplicity. Imagine it just renders this.props.children)
modal() function returns react component that triggers change in ui store. So as soon as router gets /users/login it adds login-form to ui store, <Layout /> get it as prop (or state) and renders <Modal /> which renders corresponding for given key modal.
To programmatically assess to a new URL, pass the router to your component and use push. push for example will be call in the callback trigger by the user action.
When setting your router set a route to /projects/:status. then, in your component route, you can read the value of status using this.props.param.status. Read "whats-it-look-lik" from react-router for an example.

Resources