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

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

Related

React children rerender

I have two layouts one for mobile second for desktop view. Thier structure is different however both inject the same childrens. I would like to prevent childrens rerender when the layout switch. Here is pseudocode which reflect the case(or check live https://playcode.io/1193034):
import React, { useState } from "react";
interface ParentProps {
children: React.ReactNode;
}
const Parent = ({ children }: ParentProps): JSX.Element => {
const [value, setValue] = useState(false);
if (value) {
return (
<>
<button type="button" onClick={() => setValue((val) => !val)}>
Turn into false
</button>
{children}
</>
);
}
return (
// some additional elements for desktop view
<div>
<div>
<button type="button" onClick={() => setValue((val) => !val)}>
Turn into true
</button>
{children}
</div>
</div>
);
};
const ChildrenComponent = () => {
console.log("rerender Children...");
return <p>children component</p>;
};
export const App = ()=> {
return (
<Parent>
<ChildrenComponent />
</Parent>
);
}
What could I do?
So far I have tried to wrap children component in Rect.memo and useMemo (inside Parent) both didn't work. Maybe it's just impossible to do
You will not be able to avoid rerendering in your scenario, as you are rendering different elements in different positions
The way React knows what elements to rerender is by creating a tree structure (the virtual DOM) and comparing it to the browser DOM. When a node in the virtual DOM changes in relation to the browser DOM, it will be replaced (and therefore rerendered), along with every child element of this node. This process is called reconciliation.
Because of this, even if some of the child components are the same, once you change the position or the type of their parent elements, there is no way to avoid their rerendering.
On a sidenote, you could avoid the rerendering if you restructure your component to always return the same types of elements in the same positions, and make them responsive by using CSS media queries.

How can I render one component conditionally twice and not lose internal states/unmounts?

I have one component which needs to be rendered conditionally. Renders the same component with different styles. So, I did like this
import ComponentToRender from '../../ComponentToRender'
const Main =()=> {
const [expand,setExpand] =useState(false)
return (
<div>
{!expand && <ComponentToRender {...someProps} />}
{expand && <div>
<ComponentToRender {...otherProps} />
</div>
}
<button onClick={()=>setExpand(pre => !pre)}>Expand</button>
</div>
)
}
For the above code, I get what I want in terms of UI. But, all the internal states are lost. I must render two components like that and keep the internal states. Is that possible to do that in React?
You can achieve this by keeping the component rendered unconditionally and hiding it with CSS.
You get to preserve Component‘s state for free along with the DOM state (scroll, focus, and input position). However, this solution has drawbacks, too:
You mount the component on startup, even if the user never accesses it.
You update the component even when it’s invisible.
import ComponentToRender from "../../ComponentToRender";
const Main = () => {
const [expand, setExpand] = useState(false);
return (
<div>
<div style={{ display: expand ? null : "none" }}>
<ComponentToRender {...someProps} />
</div>
<div style={{ display: !expand ? null : "none" }}>
<div>
<ComponentToRender {...otherProps} />
</div>
</div>{" "}
<button onClick={() => setExpand((pre) => !pre)}>Expand</button>
</div>
);
};
The reconciliation algorithm is such that when on next render you move from one component to component of different type (assuming they have same spot in component hierarchy), instance of old component is destroyed.
Since you have <ComponentToRender/> and another one is <div><ComponentToRender/></div>, they are different components (because one is inside a div).
Read about reconciliation.
What you can do is move the state of ComponentToRender to Main and pass it as props. Now even if the component unmounts the state will not be lost.

MaterialUI Tabs not responsive

I'm trying to create an appbar with tabs on it and then render it into my page using reactDOM, and it renders fine but clicking on a tab won't change tab for some reason.
What I tried so far:
printing the value state and checking that it actually changes (it does)
using the MUI example and seeing if it's a browser thing (it isn't)
Rendering the appbar, tabs, and tabpanels separately which led to more issues.
Some relevant code:
//this code is inside a component
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
//json_links is a list of jsons that provides user information
const ListOfTabs = json_links.map((link, index) => (
<Tab value={index} label={link['userName']} {...a11yProps({index})} />
))
//TabPanel is taken from the material-ui example on tabs
const ListOfTabPanels = json_links.map((link, index) => (
<TabPanel value={value} index={index}><Profile imagelist={link['images']} username={link['userName']}/></TabPanel>
))
let TabsElement = <div><AppBar id="tabsAppBar" position="static"><Tabs value={value} onChange={handleChange} aria-label="simple tabs example">{ListOfTabs}</Tabs></AppBar>{ListOfTabPanels}</div>
ReactDOM.render(TabsElement, document.getElementById("root"))
Expected behavior: users show in different tabs and a click switches users
Actual behavior: users show in different tabs and a click does nothing, it changes the state but the tab elements aren't "notified" of the change and don't respond

Using Stateful React classes in typescipt

I am trying to create a Stateful class in which you can call methods such as createHeaderButton() where after calling it would update the state and re-render with these new updates in the component.
Im using Material-UI and so most of their styling utilizes Reacts hook API which of course classes cant use. Ive tried to get around this by using;
export default withStyles(useStyles)(HeaderBar)
Which exports the class separately with the Styles(withStyles(useStyles) useStyles as the defined styles) And the class(HeaderBar). Now the only issue is that i need to access the styles in my class. Ive found a JS example online that wont work for me because of the strong typed syntax of TS. Additionally When initializing my Class component in other places i try to get the ref=(ref:any)=>{} And with that call the create button methods when i get a response from my server, Which doesnt work because of this new way of exporting the class component!
Thanks for the help, Heres my component class: https://pastebin.pl/view/944070c7
And where i try to call it: https://pastebin.com/PVxhKFHJ
My personal opinion is that you should convert HeaderBar to a function component. The reason that it needs to be a class right now is so you can use a ref to call a class method to modify the buttons. But this is not a good design to begin with. Refs should be avoided in cases where you can use props instead. In this case, you can pass down the buttons as a prop. I think the cleanest way to pass them down is by using the special children prop.
Let's create a BarButton component to externalize the rendering of each button. This is basically your this.state.barButtons.forEach callback, but we are moving it outside of the HeaderBar component to keep our code flexible since the button doesn't depend on the HeaderBar (the header bar depends on the buttons).
What is a bar button and what does it need? It needs to have a label text and a callback function which we will call on click. I also allowed it to pass through any valid props of the material-ui Button component. Note that we could have used children instead of label and that's just down to personal preference.
You defined your ButtonState as a callback which takes the HTMLButtonElement as a prop, but none of the buttons shown here use this prop at all. But I did leave this be to keep your options open so that you have the possibility of using the button in the callback if you need it. Using e.currentTarget instead of e.target gets the right type for the element.
import Button, {ButtonProps as MaterialButtonProps} from "#material-ui/core/Button";
type ButtonState = (button: HTMLButtonElement) => void;
type BarButtonProps = {
label: string;
callback: ButtonState;
} & Omit<MaterialButtonProps, 'onClick'>
const BarButton = ({ label, callback, ...props }: BarButtonProps) => {
return (
<Button
color="inherit" // place first so it can be overwritten by props
onClick={(e) => callback(e.currentTarget)}
{...props}
>
{label}
</Button>
);
};
Our HeaderBar becomes a lot simpler. We need to render the home page button, and the rest of the buttons will come from props.childen. If we define the type of HeaderBar as FunctionComponent that includes children in the props (through a PropsWithChildren<T> type which you can also use directly).
Since it's now a function component, we can get the CSS classes from a material-ui hook.
const useStyles = makeStyles({
root: {
flexGrow: 1
},
menuButton: {
marginRight: 0
},
title: {
flexGrow: 1
}
});
const HeaderBar: FunctionComponent = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<HeaderMenu classes={classes} />
<Typography variant="h6" className={classes.title}>
<BarButton
callback={() => renderModule(<HomePage />)}
style={{ color: "white" }}
label="Sundt Memes"
/>
</Typography>
{children}
</Toolbar>
</AppBar>
</div>
);
};
Nothing up to this point has used state at all, BarButton and HeaderBar are purely for rendering. But we do need to determine whether to display "Log In" or "Log Out" based on the current login state.
I had said in my comment that the buttons would need to be stateful in the Layout component, but in fact we can just use state to store an isLoggedIn boolean flag which we get from the response of AuthVerifier (this could be made into its own hook). We decide which buttons to show based on this isLoggedIn state.
I don't know what this handle prop is all about, so I haven't optimized this at all. If this is tied to renderModule, we could use a state in Layout to store the contents, and pass down a setContents method to be called by the buttons instead of renderModule.
interface LayoutProp {
handle: ReactElement<any, any>;
}
export default function Layout(props: LayoutProp) {
// use a state to respond to an asynchronous response from AuthVerifier
// could start with a third state of null or undefined when we haven't gotten a response yet
const [isLoggedIn, setIsLoggedIn] = useState(false);
// You might want to put this inside a useEffect but I'm not sure when this
// needs to be re-run. On every re-render or just once?
AuthVerifier.verifySession((res) => setIsLoggedIn(res._isAuthenticated));
return (
<div>
<HeaderBar>
{isLoggedIn ? (
<BarButton
label="Log Out"
callback={() => new CookieManager("session").setCookie("")}
/>
) : (
<>
<BarButton
label="Log In"
callback={() => renderModule(<LogInPage />)}
/>
<BarButton
label="Sign Up"
callback={() => renderModule(<SignUpPage />)}
/>
</>
)}
</HeaderBar>
{props.handle}
</div>
);
}
I believe that this rewrite will allow you to use the material-ui styles that you want as well as improving code style, but I haven't actually been able to test it since it relies on so many other pieces of your app. So let me know if you have issues.

Material UI Nested lists using recursive render is not working with react hooks

I'm working on a project in which I need a sidebar with nested lists created with material UI, if I go to demo and copy the nested list code it's all fine but it comes with alot of code duplication. So after some digging I found recursive rendering function gist which is written with classes. I'm using react hooks and the function logic is not working as expected.
Problem:
I have 3 level navigation, A parent, then child and child contains more items (grandchildren to parent). So when I click on the parent everything is fine children items appears, but when I click any of the children element the whole list closes.
here is my Code:
// useState hook with empty object as an initial Value
const [open1, setOpen1] = React.useState({});
// this method sets the current state of a menu item i.e whether it is in expanded or collapsed or a collapsed state
const handleClick = (param) => {
setOpen1(prevState => ({[param]: !prevState[param]}));
};
// if the menu item doesn't have any child, this method simply returns a clickable menu item that redirects to any location and if there is no child this method uses recursion to go until the last level of children and then returns the item by the first condition.
function nestedMenu(items) {
return items.map((nav) => {
if (!nav.children) {
return (
<div key={nav.id}>
<ListItem button>
<ListItemText primary={nav.name} />
</ListItem>
</div>
)
}
return (
<div key={nav.id}>
<ListItem button onClick={()=>handleClick(nav.name)}>
<ListItemText primary={nav.name} />
{open1[nav.name] ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open1[nav.name] } timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{ nestedMenu(nav.children)}
</List>
</Collapse>
</div>
)
})
}
After that I'm calling the nestedMenu function with json data as parameter in my return method
<List>
{nestedMenu(Links.items)}
</List>
Can anyone please explain what I'm doing wrong in all this, I have been working my head out to locate the problem. Any help would be much appreciated.
the problem is you change the whole open1 object :
const handleClick = (param) => {
setOpen1(prevState => ({[param]: !prevState[param]}));
};
for example if u have {p1:true, p2:false} and execute handkeClick('p1'),
open1 will be {p1:false}.
you need to keep the other members of open1 unchange and just update the one you want :
const handleClick = (param) => {
setOpen1(prevState =>
retrun {...prevState ,[param]: !prevState[param]}
);
};

Resources