Does React have keep-alive like Vue js? - reactjs

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

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

Ag grid using framework cell renderer keeps re-rendering on any store change

Trying to implement a custom cell framework renderer with React and it seems simple enough. Create the React component, register it with frameworkComponents.
The data that populates rowData is coming from my redux store. The custom cell renderer is a functional react component.
The issue is that because I'm using a frameworkComponent - a React component in my case - as a cellRenderer, it seems that any change in the data for the grid the I'm getting via useSelector(selectorForMyData) causes a re-render of my frameworkComponent, which on the browser, looks like a random, annoying flicker. The application is heavily wired into redux
Two questions:
1 - How come when I natively use ag grid to render this cell using a AgGridColumn without any custom cell renderers, it doesn't cause this re-rendering behavior on the same store changes? I have a click event bound to the main page that toggles a flag to false (in the case a snackbar alert was open).
2 - Is there any way to get around this? I've tried wrapping my return statement in the framework cell renderer component with a useMemo with the params as a dependency, but that doesn't seem to work. Also tried making a render function via useCallback with the same idea as useMemo and that doesn't help either :/
Thanks
pseudo-code for situation:
App.tsx:
<MyAgGrid/>
MyAgrid.tsx:
const MyAgGrid = () => {
const data = useSelector(selectorForData);
return (
<AgGridReact
rowData={data}
frameworkComponents={
{'myCustomRenderer': CustomRendererComponent}
}
columnDefs={
['field': 'aField', cellRenderer: 'myCustomRenderer']
} />
);
};
CustomCellRendererComponent.tsx:
const CustomCellRendererComponent = (params) => {
console.log("params", params) //logs on every redux store update
return (
<div>HELLO WORLD</div>
);
};
The cells that are rendered via the CustomCellRendererComponent are re-rendered on any redux store change. I'm guessing it's due to useSelector causing the re-render on store changes, which is expected, but then it doesn't answer my first question.
EDIT:
I went "function as class" route shown here ("MyCellRenderer") and so far am not seeing the re-rendering issue, so I will stick with that for now even though it's god ugly. This leads me to believe my issue is trying to fit a React component/hooks, with its lifecycle nuances, as a cell renderer is causing problems. Still, I feel like there should be a way to prevent the behavior of constant re-rendering, otherwise it's a pretty useless feature
EDIT pt 2:
I dug deeper and while I haven't found an out of the box solution for it, I added the reselect library to memoize some of my selectors. The selector I use to get rowData is now memoized, and I'm no longer seeing this issue. Will mark as answer in a few days if no one provides a better, ideally out of the box (with redux or ag grid), solution for it.
As I stated in one of my edits. I figured it out, kind of.
I added the library reselect to the application, and it fixes the symptoms and I'm content with it going forward. It allows you to memoize your selectors, so that it only registers changes/"fires" (leading to a re-render) only if the any of the dependency selectors you hook it into changes/fires, so now, my grid doesn't "flicker" until the actual row data changes since my selectorForRowData is memoized!
I'm still uncertain why prior to using a frameworkComponent (React Component) as a cell renderer I wasn't seeing the symptoms, but I'm happy to just assume Ag-Grid has a lot of performance tricks that clients may lose when plugging in their own custom functionality, as is the case in a custom cell renderer.

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 update match.params?

The react app has search page. There are input.
The path is 'search/:query', and by default you see zero results.
If you go to 'search/star%20wars' you will see some results. In componentDidMount() I added if statement to load result if match.params.query is not null.
If I type into search input Spider Man and click submit - I trigger a search and show results. But if you reload page - you will see the result about Star Wars. So how update match.params.query? Or may be there other solution of fix this.
You need to update the history object as well.
What you are doing is altering the history object available to you and calculating the results based on that object. But when you will refresh the page it still holds the original history object.
One way of doing it, you need to push or replace a new route in the history.
Because evert search page is a new page, so if you want the previous pages to stay preserved you should use history.push otherwise history.replace
Implement it like this:
var routeObj = {
pathname: samePath,
state: sameState,
query: newQuery
}
//push it in your history using which ever routing library you are using.
//For Example:
router.history.replace(routeObj);
Note: Do not worry about rendering speed on changing the history. React is smart enough to handle that. Basically whenever you will push a route whose component is already mounted it will not unmount and remount the same component again, rather it will just change the props and will re render it.
The callback for this case will be => componentWillReceiveProps
#misha-from-lviv The way I see your problem statement is that you have two source of truth on is the query params, using which you should update your state, and the other is the default state which is populated from the default value of your filters.
As #Akash Bhandwalkar suggested, you do need to update the route in using the History API. But also you also a need a top-level orchestrator for your application state, which will allow you to read and write to the history api ( change your route ) and also do an XHR / fetch for you to get the results.
How I'd approach this is that I'd start with a Parent component, namely FiltersContainer , which actually does this orchestration to read and write to the url. This Container would have all the side-effect knowledge for fetching and updating the routes ( error handling included ). Now the all the child components ( filters and search results maybe ) will just read the state thus orchestrated and re-render.
Hope this guides your thinking. Do revert here if you need further guidance. 😇
Cheers! 🍻

ReactJS issue on my test app

So, I've been working through my first ReactJS app. Just a simple form where you type in a movie name and it fetches the data from IMDB and adds them as a module on the page. That's all working fine.
However each movie module also had a remove button which should remove that particular module and trigger a re-render. That's not working great as no matter which button you click it always removes the last movie module added rather than the one you're clicking on.
App:
http://lukeharrison.net/react/
Github codebase:
https://github.com/WebDevLuke/React-Movies
I'm just wondering if anybody can spot the reasoning behind this?
Cheers!
Just a hunch, but you should use a unique key, not just the index of the map function. This way React will understand that the movies are identified not by some iterating index, but an actual value, and that will probably solve your issue.
var movies = this.state.movies.map(function(movie, index){
return (
<Movie key={movie} useKey={index} removeMovieFunction={component.removeMovie} search={movie} toggleError={component.toggleError} />
);
});
This is because React re-evaluates your properties, sees that nothing has changed, and just removes the last <Movie /> from the list. Each Movie's componentDidMount function never runs more than once, and the state of Movie 1, Movie 2 and Movie 3 persists. So even if you supply search={movie} it doesn't do anything, because this.props.search is only used in componentDidMount.
I'm not exactly sure why it isn't rendering correctly as the dataset looks fine.
Looking at the code, I would change your remove function to this...
var index = this.state.movies.indexOf(movieToRemove);
console.log(this.state.movies);
if (index > -1) {
this.state.movies.splice(index, 1);
}
console.log(this.state.movies);
this.setState(this.state.movies);
My assumption is that, the state isn't being updated correctly. Whenever updating state, you should always use setState (unless the convention changed and I wasn't aware).
Also, you shouldn't need to explicitly call forceUpdate. Once setState is called, React will automatically do what it needs to and rerender with the new state.
State should be unidirectional (passed top down) from your top level component (known as a container). In this instance, you have state in your top level component for search strings and then you load individual movie data from within the "Movie" component itself via the IMDB API.
You should refactor your code to handle all state at the top level container and only pass the complete movie data to the dumb "Movie" component. all it should care about is rendering what you pass in it's props and not about getting it's own data.

Resources