React-Router v4 (List / ListItem, Routes, Animations) - reactjs

I just ran into a problem with react router v4, maybe it's not a big deal, but I don't know how to handle this the right way.
It should look something like this:
<Route path="/" component={List}>
<Route path="/listitem/:id" component={ListItem} />
</Route>
If I am now on the ("/" - List)-page I want to see all the ListItems from the different ("/listitem/:id" - ListItem)-pages. (with less informations but this should be a simple css-problem)
As soon as I click at one, I want to turn that one fullscreen (animations and stuff... but that's not the problem here) and the route should change, unsurprisingly :-).
If I am now on one specific ListItem and trigger history.goBack(), I want to animate that Item back into the List and the route should change back to "/".
Additional (maybe optional) thought: If I visite the site initially at a "/listitem/:id"-page and click on a Link to the "(/" - List)-page, there shouldn't be any animation of the ListItem.
I guess it's a basic problem for react-router, but I actually don't know right now how to implement this properly.
I don't know if it makes any difference, but I am using Redux aswell.
I hope, someone can help me out.
Thank you in advance.

I think this would be best handled with state in the Parent component. For example:
this.state = {
fullscreen: false,
idOpened: null
}
Once you click on the child component (the list item), you set a callback that sends the ID to the parent which then triggers the fullscreen-mode. This way you won't need to use the history API and the visitors won't have to wait for page loads (except for the initial one).
It'll also make it easy to not trigger any animations when clicking on another list item.

Related

React don't mount component until needed but don't unmount afterwards in list of components

Say I am building an instant messaging with app with React (I'm not doing that exactly, but this is easier to explain). I have a sidebar with a list of conversations and, when you click one, it is shown on the right (similar to this). I don't want to mount each conversation component until the user clicks it, but I don't want to unmount it, just hide it, when they click on another conversation. How can I do this cleanly? There will never be more than about 30 chats for any user.
You can store the enabled conversations in an array that you use to show, and when you disable a conversation you can just add a hidden prop to it which you pass to the conversation and make it return null. This will make it not render anything but will not unmount it since you have not removed it from the array that handles the display of conversations.
example at: https://codesandbox.io/s/wispy-forest-59bqj
This is a bit hard to answer since you haven't posted the code.
But, theoretically, the best way to approach this problem is to transfer the data from your sidebar component and load it onto the right component on a per-user basis. You don't have to mount each "conversation component".
You can do this by with the boolean hidden property in your markup. React will render as usual and simply pass it along to the html, the browser will then simply not paint it.
const HideMe = ({ isHidden }) => (
<div hidden={isHidden}>
can you see me?
</div>
)
I made an example for you:
https://codesandbox.io/s/elastic-curie-t4ill?file=/src/App.js
reference: https://www.w3schools.com/tags/att_hidden.asp

How to pass a set of React Nodes to unrelated components

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.

How to override Back button when a drawer is showing in React 16 / Router V4

I have a structure like this, there's a route like:
<Route path="/sample-route" component={ComponentA}/>
Then ComponentA has:
<ComponentA>
<ComponentB/>
<ComponentC>
<MaterialUIDrawer/>
</ComponentC>
</ComponentA>
ComponentC is used in 5 different routes as a child. The MaterialUIDrawer is showing based on a flag in a redux reducer. The problem I'm trying to solve is when the drawer is open, clicking back hides it but also navigates back. I tried solving it like this:
window.onpopstate = (e) => {
if (this.props.isOpen) {
this.props.toggleDrawer(false);
this.props.history.replace(this.props.match.url);
}
};
This has 2 issues:
If this is the first page you land on, it doesn't actually do anything, the function doesn't trigger
If you are on a different site and navigate to the url that contains the drawer, hitting Back moves you to a different URL (different domain)
I also tried setting a <Route/> in ComponentC and then the drawer lives there, but I didn't manage to get it to work, maybe the path is wrong. It felt like this might be the right way though, if path is /path1, then drawer lives in /path1/drawer, or /path2/drawer, etc.
I'm trying to find a way for the Back button in a browser to close the drawer (so execute a function I define) and override the default functionality.
I think you should be using props here instead of paths, so something like /path1?drawer=1, but you will definitely need to use history/location so that the back button can actually go back, so you are on the right trail.
I'm not sure what you are using for a browser history manager, but I would recommend tying off of that instead of leaning on the window pop-state. Your history module should be the source of truth and feed redux, not the other-way around IMO.

react-transition-group conditionally animate out

I'm trying to use react-transition-group to add some animation to a list that I have.
This is my general setup, I have list component that renders a number of children. There are a number of interactions that cause items to be added/removed from this list. Some of these actions need to cause the component to be removed using an animation but I can't seem to get that working and I'm guessing maybe I'm missing something here.
This is my basic setup:
<TransitionGroup className="list" component="ul">
{this.props.item.map(item => (
<CSSTransition key={item.id} timeout={500} classNames="fade" enter={false} exit={!!item.shouldAnimate}>
<ChildComponent/>
</CSSTransition>
))}
</TransitionGroup>
I have verified that everything is fine in my state logic (i.e. shouldAnimate is set to true as I expect but it still doesn't animate.
Any suggestions?
Edit:
Created a fiddle showing the problem and I think I now see the issue.
http://jsfiddle.net/af0ee2eo/2/
As I described above I don't know until the user takes an action if that action should be one that causes the item to be animated or not when it is removed from the list. I'm setting shouldAnimate correctly but then before the next render I'm removing that item from the list. If I delay the removal until after the next render things work okay but that's annoying to have to do every time (this is what I am demonstrating by the "Use Delay?" checkbox at the bottom of the fiddle). I wish there was a way to inject state into the state of the item held by TransitionGroup's component state.
If anyone has any other idea how I might fix this without a setTimeout or requestAnimationFrame render hack let me know otherwise I'll just mark this question as answered.
I've figured it out and just in case someone else comes along and see this the answer is to use the childFactory prop. It allows you to change prop values on leaving children.
Link with more info: https://medium.com/lalilo/dynamic-transitions-with-react-router-and-react-transition-group-69ab795815c9

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

Resources