How to pass a set of React Nodes to unrelated components - reactjs

I have a question about React, here's a simplified version of a React app.
In the app I want to render a fixed primary menu and a secondary menu that is optional and its content is controlled by inner components rendered in routing.
Also secondary menu is rendered somewhere else in mobile version of the app.
function App() {
return <Router>
<PrimaryMenu/>
<SecondaryMenu/>
<LayoutContent/>
{/* This block is rendered only on mobile devices */}
<Responsive {...Responsive.onlyMobile}>
<SecondaryMenu/>
</Responsive>
</Router>;
}
LayoutContent will render actual page content (using a Page component) according to routing rules and every page component may render its own secondary menu like this (e.g. page1 has its own submenu, page2 has another one, page3 has not.)
<Page title='Page 1 - With secondary menu'>
<SecondaryMenuItems>
{/* I want this content as children of secondary menu in both mobile and desktop menubars */}
<li>Page 1 item 1</li>
<li>Page 1 item 2</li>
</SecondaryMenuItems>
</Page>
I tried to implement it by using React Contexts but if I store children nodes in context an infinite render is triggered. I changed it to use a id property in <SecondaryMenuItems/> component but the approach is very fragile and also has some drawbacks.
Here's my working example it's working but as I said is pretty fragile:
What if I use a duplicate id for secondary menus?
What if I forget a secondary menu key?
Also if you switch to a page with a menu and then go to page3 (that has no menu) previous page menu remain on screeen.
How to accomplish this with react? Is there a suggested way to do that?
A simpler way to express my question is "how to pass a set of react nodes between unrelated components (e.g. siblings components)"
Update
I've completed my working example with received hints, now by combining useRef with ReactDOM.createPortal I achieved final result which is now in the example.

This is a use case for React Portals. Portal will let you render secondary menu items from a page into secondary menu container that exists somewhere else
All you need to do is to call React.createPortal in render of thepage, pass rendered element and target node to render into, regardless of position in DOM tree
I've edited your example using portals here https://codesandbox.io/s/secondary-menu-example-vbm3x. This of course is a basic example, you might want to abstract portals logic in a separate component for convenience, and/or pass dom reference from parent, instead of calling getElementById on mount

Rendering same children in multiple sibling nodes
The question asks "how to pass a set of react nodes". Ideally, don't. If you are rendering nodes somewhere in your hierarchy with the intention of using them elsewhere, you may be using the wrong strategy.
If you need to render the same components in different places, make a function that renders the components, and call it from both places. In other words, always pass the information, not the rendered elements.
Render inside the router
In a typical Single Page Application, the router will render all of the (non-static) components. This is how the example should have done it. The routing component (LayoutContent) should have been responsible for rendering the "passed nodes" (SecondaryMenu) directly.
<Route path="/page1">
<Page title="Page 1 - With secondary menu">
<SecondaryMenu id="menu1"> {/* <- use SecondaryMenu instead of SecondaryMenuItems */}
<li>Page 1 item 1</li>
<li>Page 1 item 2</li>
</SecondaryMenu> {/* <- use SecondaryMenu instead of SecondaryMenuItems */}
</Page>
</Route>
When rendering inside the router is impossible
If for some reason the routing component cannot render the content directly, then a Single Page Application (or routing) solution is probably the wrong solution here. The question doesn't include any information as to why the components can't be rendered inside the router, feel free to edit the question and comment with more info.
Another way of achieving the example would be for there to be no routing component (i.e. no LayoutContent) and for SecondaryMenu to check the path of the page and conditionally render the appropriate content based on that.
It may seem silly to manually render conditionally based on a path when there is a router component which does this for you, and I would agree. The solution is then to not use a router at all. Trying to render children in the router and passing them has a strong code smell.
In the React hierarchical layout, if the same information is needed make decisions about rendering in multiple places (the path in this case), move that information up to the nearest parent of all components and pass it down as props or as context.
Avoiding ID clashes
"What if I use a duplicate id for secondary menus?"
If you call a function to render the secondary menu instead of rendering it and passing it, then you can pass a menu prefix in the props, and use this menu prefix in the function.
function SecondaryMenuItems({ children, idPrefix, path }) {
if (path == '/path1') {
return (
<ul id={`${idPrefix}-newlist`}>
On keys
"What if I forget a secondary menu key?"
React keys need only be unique within a rendered list. In fact, keys are simply an optimisation to prevent React having to re-render a generated list on every pass. If you forget to include a key (or make a bad choice of key), React has to re-render the list every time, but it's not more serious than that. In simple cases, you won't notice the drop in performance.

Related

Is there a counterpart to Angular "slots" for DOM nodes in React?

The goal is to create a W3C web component in React which supports arbitrary DOM nodes as children.
The initial markup in the browser should be like this:
<custom-button>
some <u>styled</u> text here
</custom-button>
I would then:
call customElements.define() to register my React code as the implementation of the custom-button,
inside the implementation create a shadow root inside <custom-button>,
afterwards call ReactDOM.render(<CustomButton ...>, shadowRoot); to populate this shadow root
The DOM structure in the browser is now:
<custom-button>
#shadow-root
<div class="CustomButton">
<!-- x -->
</div>
some <u>styled</u> text here
</custom-button>
But this is not really the desired results; I need the original content of <custom-button> to render inside <div class="CustomButton"> now.
I am aware of the React children prop, but as I understand it, it will only work for children that were also declared inside the React implementation and not with arbitrary DOM nodes that were created on the surrounding web component element.
On the other hand I read that Angular implements a concept they call "transclusion" in which they provide slots which DOM children web component will be mapped into. Is there anything similar in React?
One quick "workaround" that I could think of in React is to:
obtain the ref of the top-level JSX tag,
find the shadow root in ref.parentNode
iterate through all children and re-attach them to ref as new parent
This would only cover the initialization though. If, at runtime, some other script tried to append more children to <custom-button>, or re-order or delete previously inserted children, this would probably fail.
I have just realized that the <slot /> of the web components standard solves the problem.
So, to reflect the content of the <custom-element> inside the React component, the following is completely sufficient:
render() {
return (
<div className="customElement">
...
<slot />
...
</div>
);
}

How to target specific components through Links in react-router?

I want to insert links throughout the content of a React app, where links trigger 'navigation' only within specific Components in the tree. Ideally I would use a react library to do this, rather than invent my own framework from scratch.
Existing approaches that I can find, such as react-router, seem to assume that every routed component should only be visible when a route path matches it, rather than routes being able to selectively send 'control' signals to matching components, while unmatching components should not be affected at all.
My intended application needs independent navigation within different panes, similar to the behaviour of a HTML Frameset ( see e.g. this JavaDoc single-page navigation - https://docs.oracle.com/javase/7/docs/api/ ) where specific links have a href (a route in React) but also a 'target' which indicates which pane needs to be affected by a given navigation.
I am aware I could write a bespoke React eventing pattern. For example I could pass hooks to make changes through tree state, with my own bespoke hash or history eventing in place to monitor clicks. Before I consider writing my own framework for this, I want to understand what other approaches there are and I think I must be overlooking something obvious.
I have put together a repository which simplifies the problem from a react-router point of view.
https://github.com/cefn/graphql-gist/tree/fde58e9cf5d321d1edf3b916da4bdd95b79751a1/react-router-frames
This app has 'Frames' with embedded links. However, every Frame's component in the React tree has to be switched out for another matching component (or none) when a Link is clicked. Ideally I should be able to add a 'target' attribute or otherwise specialise a Link or target so that only a targeted part of the tree is affected by a matching link.
It should be possible for example, to cause the color of the name='left' or name='right' frames to change independently in https://github.com/cefn/graphql-gist/blob/fde58e9cf5d321d1edf3b916da4bdd95b79751a1/react-router-frames/src/FrameSet.js
I am hoping for something from the mainstream react ecosystem which supports routing (e.g. well-documented components with hash listening, history support) but not where every Link affects every Route in the page.
Here is a solution which exploits react-router and isn't totally horrible.
function FrameSet(props) {
return <Router>
<FilterPath pathPrefix="/left/">
<View />
</FilterPath>
<FilterPath pathPrefix="/right/">
<View />
</FilterPath>
</Router>
}
A FilterPath always passes on its pathPrefix value to its childrens' props, and optionally, (when the react-router location matches the pathPrefix) passes on a pathSuffix too.
In this way, each View above only receives a pathSuffix update when a route path begins with their pathPrefix and hence 'targets' them.
A draft working implementation of FilterPath is...
function FilterPath(props) {
return <Route render={({ location: { pathname } }) => {
const { pathPrefix } = props
let pathSuffix
if (pathname.startsWith(pathPrefix)) {
pathSuffix = pathname.slice(pathPrefix.length)
}
return React.Children.map(props.children, child => React.cloneElement(child, { pathPrefix, pathSuffix }))
}} />
}
A working example using FilterPath can be seen at https://github.com/cefn/graphql-gist/blob/2de588be6c2d30b92d452f71749377c9dc6c19c7/react-router-frames/src/FrameSet.js#L22

Real-Time use case of this.props.children

I have read many articles to find out the real time use case of this.props.children but i didn't find the answer that i am looking for.I know that this.props.children is used to access the data b/w the opening and closing tag of a component. But my question is why can't we add a prop to the component instead of writing data b/w opening and closing tag.
for Ex:
<Example>This is data<Example> //can be accessed as this.props.children
can be written as
<Example data="This is data"/> //can be accessed as this.props.data
Can somebody please explain me with a real-time example of where we can achieve a certain task by using only this.props.children?
For example if you have complicated children of a component:
<Card>
<div class='title'>Title</div>
<div class='content'>Content</div>
</Card>
It would be easier than if you write like:
<Card content={[<div class='title'>Title</div>, <....>]} />
Samething you can find here, for example in Drawer component of Material-UI here. Drawer is a component that slides from the left, it can contain anything, so using props.childrens.
While making an app, you want a parent component which will render anything in your component. The use cases which I can think of are:
When you want to open a different component depending upon the route change.
const App = ({ children }) => (
<div className="full-height">
{children}
</div>
);
When you want to have same styles throughout your app for generic elements such as body, head etc. You'll just have to apply on this component, e.g., in above example, the full-height will get applied everywhere in the app on top component. (Obviously there are other work arounds but this is always more clear)
For use cases where you want to expose your component (when component doesn't know children ahead of time) as libraries and props can vary a lot and complicates the rendering. Read this
Obviously you don't have to use it but it makes code more elegant and understandable.

Does React have keep-alive like Vue js?

I made a Todo list with React js. This web has List and Detail pages.
There is a list and 1 list has 10 items. When user scroll bottom, next page data will be loaded.
user click 40th item -> watch detail page (react-router) -> click back button
The main page scroll top of the page and get 1st page data again.
How to restore scroll position and datas without Ajax call?
When I used Vue js, i’ve used 'keep-alive' element.
Help me. Thank you :)
If you are working with react-router
Component can not be cached while going forward or back which lead to losing data and interaction while using Route
Component would be unmounted when Route was unmatched
After reading source code of Route we found that using children prop as a function could help to control rendering behavior.
Hiding instead of Removing would fix this issue.
I am already fixed it with my tools react-router-cache-route
Usage
Replace <Route> with <CacheRoute>
Replace <Switch> with <CacheSwitch>
If you want real <KeepAlive /> for React
I have my implementation react-activation
Online Demo
Usage
import KeepAlive, { AliveScope } from 'react-activation'
function App() {
const [show, setShow] = useState(true)
return (
<AliveScope>
<button onClick={() => setShow(show => !show)}>Toggle</button>
{show && (
<KeepAlive>
<Test />
</KeepAlive>
)}
</AliveScope>
)
}
The implementation principle is easy to say.
Because React will unload components that are in the intrinsic component hierarchy, we need to extract the components in <KeepAlive>, that is, their children props, and render them into a component that will not be unloaded.
Until now the awnser is no unfortunately. But there's a issue about it in React repository: https://github.com/facebook/react/issues/12039
keep-alive is really nice. Generally, if you want to preserve state, you look at using a Flux (Redux lib) design pattern to store your data in a global store. You can even add this to a single component use case and not use it anywhere else if you wish.
If you need to keep the component around you can look at hoisting the component up and adding a "display: none" style to the component there. This will preserve the Node and thus the component state along with it.
Worth noting also is the "key" field helps the React engine figure out what tree should be unmounted and what should be kept. If you have the same component and want to preserve its state across multiple usages, maintain the key value. Conversely, if you want to ensure an unmount, just change the key value.
While searching for the same, I found this library, which is said to be doing the same. Have not used though - https://www.npmjs.com/package/react-keep-alive

React: how to keep sidepanel state between url changes with history.push()?

I have a fixed sidepanel that contains a search bar with filter buttons similar to https://facebook.github.io/react/docs/thinking-in-react.html
The list items are clickable and trigger history.push() for nested path urls.
The nested path structure is
/category-1
/category-1/product-1
/category-1/product-2
/category-2
/category-2/product-1
/category-2/product-2
.
.
.
/category-n/product-m
/category-n/product-k
User can scroll the sidepanel's list and select filters. However, a click on a list item (with onClick() followed by history.push()) causes the whole page (including the sidepanel) to render. This in turn doesn't keep the sidepanel's state so the filters and the scroll position are reset.
The parent render: function() looks like:
return(
<div id="main-view">
<Sidepanel history={this.props.history} properties={this.props.properties} />
React.cloneElement(this.props.children, {this.props});
</div>
);
The React.cloneElement() is the product info container that should render only.
I'm using react-router, react-redux and redux-simple-router.
Do I need to store the sidepanel's list scroll position and filter values outside of the sidepanel? Or is there a better way to keep the sidepanel's state between the url changes?
Your architecture is not aligned with what you want to do.
Simply render the navigation in your top-level, parent component and it will never lose state - for example in your Application component. Then SidePanel wouldn't be affected by route change.
Also you could pass down a callback to set / alter navigation from children, if needed (for example hide() or addMenuItems()).

Resources