React tabs - switching destroys component, need to maintain the same component - reactjs

I am trying to make a multi-tabbed SPA with React and Material-UI. I use code from this demo as an example: https://codesandbox.io/s/qlq1j47l2w
It appears that if I follow the aforementioned example, I end up returning new instance of the component with tab contents each time I navigate between the tabs:
<Tabs
value={value}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary"
scrollable
scrollButtons="auto"
>
<Tab label="Tab 1" />
<Tab label="Tab 2" />
</Tabs>
</AppBar>
{value === 0 && <Tab1Contents/>}
{value === 1 && <Tab2Contents/>}
As Tab1Contents is a form, I would like its internal state to be retained, instead of loading a new instance of the component, which the code above appears to do.
What is the best way to get React to use only one instance of the component and 'memorise field values'?
EDIT
I have added Redux to the example, but the store corresponding to the form within the Tab is destroyed the moment I switch away. Is there any other way to implement tabs in React that would hide the tab contents, instead of destroying them and re-creating them from scratch each time I navigate away?

The solution to my problem was quite simple! If you don't want to destroy the component (remove it from DOM), you can simply hide it!
Instead of:
{value === 0 && <Tab1Contents/>}
Use:
<div style={{ display: value === 0? 'block': 'none'}}>
<Tab1Contents/>
</div>

It was already mentioned that you have to prevent your component from being removed from the DOM. The easiest solution with MUI5 TabPanel is in fact to just replace
{value === index && children}
with
{children}
that means your Tabpanel would look like that:
import * as React from "react";
const TabPanel= ({ children, value, index, ...other }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`tabpanel-${index}`}
aria-labelledby={`tab-${index}`}
{...other}
>
{children}
{/* {value === index && children} // This was removed */}
</div>
);
};
export default TabPanel;
No additional logic necessary as the hidden prop already takes care of the visibility aspect. This way your components should maintain their state!

You would need to persist the state between the tab changes. I prototyped a form using React forms documentation for Tab1Container and as you play around with it, the value will disappear
https://codesandbox.io/s/material-demo-ojwhr
What you ideally need to use something like Redux, which will use a store to keep the information even between the state changes like Tab clicks.
Hope this helps!

Related

React Material UI - Prevent re-rendering child component Tabs on Tab change

I have a list of "tasks" I'd like to map where each task has its own set of tabs. In the TabPanel, there is a component TaskOrg which fetches stuff from the backend (api calls that's significant) and displays the data in a DataGrid. I have no problem isolating each tab, meaning I can click on a tab and it behaves separately from all other tabs.
const [value, setValue] = React.useState({})
useEffect(() => {
if (provincialTasks.length > 0) {
let tabs = {}
provincialTasks.forEach(task => {
tabs[task.id] = false
})
setValue(tabs)
}
}, [provincialTasks])
function TabPanel(props) {
const {children, value, index, ...other} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{p: 3}}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
...
return (
{provincialSection.legalTasks?.map((provincialTask) => {
return (
<Tabs value={value[provincialTask.id]}>
<Tab value={`org-tab-${provincialTask.id}`} {...a11yProps(`org-tab-${provincialTask.id}`)} />
</Tabs>
<TabPanel value={value[provincialTask.id]} index={`org-tab-${provincialTask.id}`}>
<TaskOrg value={provincialTask.id} {...other} />
</TabPanel>
)
}}
)
The problem is that every time something changes in the parent, everything gets re-rendered, including tabs, of course. But on re-render, the TaskOrg also resets and fetches again the data from the backend and it displays that same data. I know there is useMemo but that only works for a single component. In this case, the provincialTasks objects can vary between 10-20... that's a lot of calls.
I tried passing an active prop to the TaskOrg component to only fetch data if I'm in the right tab, but it still doesn't prevent the re-render of that set of tabs (if I switch between tab for the same task).
Is there a way to prevent re-rendering on a set of child components, namely TaskOrg components should the parent decide it changed its state?
From the code you shared I suspect that you have that function called TabPanel that returns a component within your main component. When you do that, every time the main component updates, it updates the function as well, re-rendering the TabPanel. You should declare that function outside the main component to prevent unwanted re-renders

Updating react state in parent very slow

I'm developing a react application, and where I always thought the react state updates were really fast, I now found a problem.
I have a page view with a lot of elements on it, one of the elements is this one that gets loaded in the page:
<NotesCard notes={deal.notes} updateNotes={notes => {setDeal(prevState => ({...prevState, notes}))}} />
NotesCard is a child component that only renders a material-ui Card with another react component in:
export default function NotesCard(props) {
const {notes, updateNotes} = props;
return (
<Card className="Card">
<CardHeader
title="Notities"
/>
<CardContent>
<Notes notes={notes} onChange={updateNotes} />
</CardContent>
</Card>
);
}
Notes is the last component that renders a text field and just takes the props to the TextField:
function Notes(props) {
const {notes} = props;
function updateNotes(event) {
// props.deal.notes = event.target.value;
props.onChange(event.target.value);
}
return (
<div>
<FormGroup>
{notes !== null ?
<TextField
multiline
defaultValue={notes}
onChange={e => updateNotes(e)}
rows={3}
variant={'outlined'}
label={'Notities'}
/>
: 'Geen beschrijving...'}
</FormGroup>
</div>
);
}
Is there anything that I do wrong that creates a lot of lag? The page is a big page so it might have something to do with that, but I'd think that the updates performances would still be okay.
Your goal is to fix the slow render, only then you will want to take a look at number of rerenders if necessary.
Please install the react-dev-tools which contains an option to mark components when the are being rerendered. Alternativly you can also monitor the performance over a couple of seconds and investigate the rendering. This should help you understand what renders unnecessarily on your actions.
I see a potential problem with this one:
<Notes notes={notes} onChange={updateNotes} />
If you trigger onChange the parent state is mutated. This then causes ALL children to rerender. I would think that only the single will change and a change in this component wont effect other siblings. So try to move the state as close to where its used as possible. If you trigger onChange only the should be updated. This is something easy which can fix a ton of performance problems without using react features like Memo.

System to manage reactjs applications

I get how to specify where to render the reactjs application by using the render method and specifying the html tag where it should be rendered.
What I do not understand is how you can have a list of react.js applications that is dynamically loaded into that same HTML tag.
For example there is a sidebar which is dynamically created to give a user a list of N number of react.js applications. When the user clicks on one of the links it loads that application into the HTML tag (div or whatever) container on the right.
I am sure this may be something easy but have been struggling with this concept for awhile.
Would appreciate any inputs anyone has on this.
If you truly had multiple full apps you wanted to swap out, you'd have to manually mount and unmount them. Something like a function like this, that unmounts the previous app, then mounts a new one. Example
function swapApp(App) {
const appNode = document.getElementById('app')
ReactDOM.unmountComponentAtNode(appNode)
ReactDOM.render(<App />, document.getElementById('app'))
}
But that would be a pain. So, typically, that menu and the content being changed are all part of the same react app. This app would render the menu, keep state about what item you clicked, and then render some components conditionally, depending on what was clicked.
Something like this example
function App() {
const [showingItem, setShowingItem] = React.useState(null)
return (
<>
<p><a href="#" onClick={() => setShowingItem('A')}>Show Item A</a></p>
<p><a href="#" onClick={() => setShowingItem('B')}>Show Item B</a></p>
{showingItem === 'A' ? <AppA /> : null}
{showingItem === 'B' ? <AppB /> : null}
</>
)
}

ReactJs - Conditional Rendering or hiding component

What's the de facto approach to choosing between conditional rendering or hiding the component with { display: 'none' }?
For the sake of discussion, let's say that I have a FilterComponent that holds the title of the filter, and a list of FilterItems, with name and amount.
In short, a FilterComponent could be:
Color
Blue (19)
Yellow (17)
Orange (3)
Black (7)
Green (10)
+ Show More
When hitting Show More button, more FilterItems will be displayed, i.e.
Color
Blue (19)
Yellow (17)
Orange (3)
Black (7)
Green (10)
Brown (17)
Pink (88)
White (55)
Red (32)
Purple (17)
- Show Less
Should I hide the FilterItems that are below the Show More? Or should I return null for those below and render them after updating the state with Show More?
I think there are a few ways to accomplish what you need. However, this seems to be the most practised:
{myConditionIsTrue && <MyComponent />}
In your case, it makes sense to use state. I would have a prop inside FilterComponent called showFullList
{this.state.showFullList && (
<React.Fragment>
<All/><The/><Other/><Components/>
</React.Fragment>)}
Just be weary, this mechanism is actually removing/adding to the DOM.
Generally in React it is better to not render something than to render it as hidden. Here is one related discussion:
https://discuss.reactjs.org/t/conditional-rendering-or-toggling-hidden-classes/2535/6
I would go for the "updating state" approach. That way you always have the actual filterItems that is showing in the state. So your components state is in sync and represents the current UI that is showing.
Guess there's no right or wrong in this question though =)
It would make more sense to not render the items that should not be shown until after the Show More has been clicked, and the state has updated. This way you can handle how many items should be shown by default before clicking Show More. This way instead of applying inline styles, or a special class to certain elements, you can use the exact same logic to all FilterItems, but only render X of them.
You can change initial state value of isHidden or something like that . When you click button value will be oppasite of before situtation . And when you wants to render you should give condition ;
{ isHidden &&
...
Generally, there is no significant performance differences between display: none and conditional rendering, because the browser's behaviour in both cases is nearly the same. The main difference is that if you use display: none, then node is not removing from the DOM tree, which forces some CSS pseudo-selectors like :last-child to consider a hidden node as last-child and so on. So, it is not performance-related, but mostly CSS-related. Both of approaches are ok for use, I suppose :)
My prefer two methodes:
#1 Element Variables
const button = <LogoutButton onClick={this.handleLogoutClick} />;
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
2# Inline If with Logical && Operator
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
More details here: https://reactjs.org/docs/conditional-rendering.html
An other approach based on Array.prototype.slice() method
The usage in parent component
import React from "react";
import { ColorList } from "./Color";
export default function App() {
return <ColorList colors={["red", "green", "blue"]} visibleItemsCount={1} />;
}
And the ColorList component looks like this:
import React from "react";
// This is just a placeholder component :)
function Color({ color }) {
return <div style={{ color }}>{color}</div>;
}
export function ColorList({ colors, visibleItemsCount = 0 }) {
const [showMore, setShowMore] = React.useState(false);
// Toggle value on click button
const onClick = () => setShowMore((value) => !value);
// Memoize the color list when props changed
const visibleColors = React.useMemo(() => {
// If show more items, return the whole array
// Otherwise, return a sliced array based on visible items
const count = showMore ? colors.count : visibleItemsCount;
return colors.slice(0, count);
}, [colors, visibleItemsCount, showMore]);
console.log(visibleColors);
return (
<>
<h1>Color list</h1>
<>
{visibleColors.map((color) => (
<Color key={color} color={color} />
))}
</>
<button onClick={onClick}>{showMore ? "Show less" : "Show more"}</button>
</>
);
}
Note: I uploaded the code on CodeSandbox, you can check it here
You could use a library called react-if. This library helps you wether to render or not based on a a condition.
Here is an example:
const Bar = ({ name, age, drinkingAge }) => (
<div>
<Header />
<If condition={ age >= drinkingAge }>
<Then><span className="ok">Have a beer, {name}!</span></Then>
<Else><span className="not-ok">Sorry, {name}, you are not old enough.</span></Else>
</If>
<Footer />
</div> )

Semantic UI React side bar render pusher only on visibility change (redux/rematch)

I am using react and semantic. I am using the multiple sidebar example. The idea is that the left hand sidebar offers up some menu options, and then the right hand sidebar is the sub menu based on which option from the left menu is chosen. When a sub menu item is selected, a component is added to the Sidebar.Pusher, i.e displayed on the page.
It all works except re-rendering the content of the Sidebar.Pusher. This apparently only updates when the left hand side bar's visibility changes. I am using redux/rematch to handle state, and can see that the state that holds the content of the Sidebar.Pusher is being updated, but `render() is only being called when visibility changes of the sidebar.
The content of Sidebar.Pusher is an array, and I even tried displaying on the page the length of the array, which is being updated (pushed into) each time an item on the right hand sidebar is clicked. However this doesn't cause a render() to be fired, its literally when the left hand sidebar visibility changes.
Just to note, I did see this issue, however its from last year, and the answer wasn't enough for me to be able to fix the issue. Help would be appreciated.
Structure:
Index.js renders App.js, App.js renders Menu.js (which is a semantic set of tabs). One of the menu options is Sidebar.js which renders:
<Sidebar.Pushable as={Segment}>
<Sidebar
as={Menu}
animation="overlay"
direction="right"
inverted
vertical
visible={secondaryVisibility}
width="wide"
>
{focusedList.map((el, i) => {
return (
<Menu.Item key={i} as="a" onClick={() => this.addSegment(el)}>
<Article el={el} />
</Menu.Item>
)
})}
</Sidebar>
<Sidebar
as={Menu}
animation="overlay"
icon="labeled"
inverted
// onHide={this.handleSidebarHide}
vertical
visible={primaryVisibility}
width="wide"
>
<Menu.Item
onClick={() => this.changeTab(menuItem)}
as="a"
name="menuItem"
header
>
Menu Item
</Menu.Item>
</Sidebar>
<Sidebar.Pusher style={{ minHeight: "600px" }}>
<Segment basic>
{segments.map((el, i) => {
console.log(`el ${el}`)
return <Content key={i} segment={el} />
})}
</Segment>
</Sidebar.Pusher>
and all state (secondaryVisibility etc) is stored in rematch
Thanks
I haven't been able to identify the problem based on the code you've posted, could you provide more info such as the entire Sidebar.js and maybe what's in the Content component?. My guess would be that there's a HOC or lifecycle method getting in the way.
I've created a trivial example that seems to work fine, if I understand what you're trying to accomplish: https://codesandbox.io/s/myl6xpz9py
I got it. I forgot about immutability in state. Perhaps someone will benefit from this.
I was trying to update a state array with
let tmp = prevState.contract.segments
tmp.push(segment)
this.update({ segments: tmp })
However, this won't work as tmp is a reference to prevState.contract.segments, so this won't work, as pushing to tmp is equivelent to pushing to prevState.contract.segments.
you have to have a completely new array:
const tmp = [...prevState.contract.segments, segment]
this.update({ segments: tmp })
Now it works.

Resources