Rerender MUI tree with disabled treeItems - reactjs

I have a MUI TreeView where its treeItems are rendered in a function.
The problem I have is that I want to toggle disable on every treeItem.
But when I try that, the items aren't disabled until I click somewhere.
You could see it in this sandbox
(When you use the toggle button it seems to work as I expect from the second click and forward, but using the other two you see the problem I have)
const Tree = (props: Props) => {
const handleToggle = (event: React.SyntheticEvent, nodeIds: string[]) => {
setExpanded(nodeIds);
};
const renderTreeItems = (
node: MyNode,
nodeDisabled: boolean,
) => {
return (
<StyledTreeItemRoot
key={node.id}
nodeId={'' + node.id}
label={
<Box sx={{ display: 'flex', alignItems: 'center', p: 0.5, pr: 0 }}>
<Typography
variant='body2'
>
{node.title}
</Typography>
</Box>
}
disabled={nodeDisabled}
{...props.treeItemProps}
>
{Array.isArray(node.children)
? node.children.map((node) => renderTreeItems(node, nodeDisabled))
: null}
</StyledTreeItemRoot>
);
};
return (
<TreeView
expanded={expanded}
selected={props.selectedNodeId}
onNodeSelect={(_event: React.SyntheticEvent, nodeId: string) => {
props.onSelect(nodeId);
}}
onNodeToggle={handleToggle}
{...props.treeViewProps}
>
{props.tree.map((item) =>
renderTreeItems(item, props.treeDisabled ?? false),
)}
</TreeView>
);
};
export default Tree;

Solved this by adding a useEffect listening for the change of disable prop and calling a forceUpdate function like this:
const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void;
useEffect(() => {
forceUpdate();
}, [forceUpdate, props.treeDisabled]);
Edit:
Although the first solution worked, I changed to adding the prop, which tells the tree to be disabled or not, to the key prop in the tree.
<TreeView
key={String(props.treeDisabled)}
expanded={expanded}
selected={props.selectedNodeId}
onNodeSelect={(_event: React.SyntheticEvent, nodeId: string) => {
props.onSelect(nodeId);
}}
onNodeToggle={handleToggle}
{...props.treeViewProps}
>
{props.tree.map((item) =>
renderTreeItems(item, props.treeDisabled ?? false),
)}
</TreeView>

Related

filter array function for an array inside set state is not working

Trying to get an an onclick function to delete an item (that's been clicked) from an array.
However, it doesnt delete anything. setListOfDocs is supposed to set the listOfDocs array with the clicked item gone (onClick function is a filter)
It does not work. There's no error in the console. I don't know why it's not updating the state so that the listOfDocs is the new filtered array (with the clicked item removed from the array).
Im using material ui.
function NewMatterDocuments() {
const [listOfDocs, setListOfDocs] = useState(['item1', 'item2', 'item3', 'item4']);
const [disableButton, toggleDisableButton] = useState(false);
const [displayTextField, toggleDisplayTextField] = useState('none');
const [textInputValue, setTextInputValue] = useState();
const buttonClick = () => {
toggleDisableButton(true);
toggleDisplayTextField('box');
};
//this function works
const handleEnter = ({ target }) => {
setTextInputValue(target.value);
const newItem = target.value;
setListOfDocs((prev) => {
return [...prev, newItem];
});
setTextInputValue(null);
}; //
//this function does not work....which is weird because it pretty much
// does the same thing as the previous function except it deletes an item
// instead of adding it to the array. Why does the previous function work
// but this one doesnt?
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => index !== deletedId);
setListOfDocs(result)
};
return (
<div>
<Typography variant="body2">
<FormGroup>
<Grid container spacing={3}>
<Grid item xs={6}>
<Grid item xs={6}>
Document Type
</Grid>
{listOfDocs.map((item, index) => {
return (
<ListItem id={index} secondaryAction={<Checkbox />}>
<ListItemAvatar>
<Avatar>
<DeleteIcon id={index} onClick={deleteItem} />
</Avatar>
</ListItemAvatar>
<ListItemButton>
<ListItemText id={index} primary={item} />
</ListItemButton>
</ListItem>
);
})}
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
<Button disabled={!disableButton ? false : true} color="inherit" onClick={buttonClick}>
Add
</Button>
<ListItem sx={{ display: `${displayTextField}` }} id={uuid()} key={uuid()}>
<TextField
id="standard-basic"
label="Additional Document"
variant="standard"
value={textInputValue}
onKeyDown={(e) => {
e.key === 'Enter' && handleEnter(e);
}}
></TextField>
</ListItem>
</List>
</Grid>
</Grid>
</FormGroup>
</Typography>
</div>
);
}
export default NewMatterDocuments;
I tried to change the function that wasnt working into the following:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
prev.filter((item, index) => index != currentTarget.id);
});
};
It gives the error
"Uncaught TypeError: Cannot read properties of undefined (reading 'map')"
The next map function inside the jsx doesnt work...
You should return your data after filter:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
return prev.filter((item, index) => index != currentTarget.id);
});
};
Update:
There is a problem with your first try. The currentTarget.id field is string but in your filter method, you're comparing it with the index (which is number) with !== which also checks types of the 2 variables.
So you can fix it by replacing !== with != or converting string to number.
I believe that you made a small mistake in your filtering function: you are comparing item index with deletedId, where you should compare item with deletedId
This should be correct:
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => item !== deletedId);
setListOfDocs(result)
};

ListItemButton onclick wont fire on the first click when using my own component as children

I have issues understanding why this onclick cares what the children is and how it works
<ListItemButton onClick={() => onClickResult(q)}>
<Typography variant="body1">{highlighted}</Typography>
</ListItemButton>
this examples where Typography from mui is beeing used works as aspected (first click triggers onClick)
but using allmost the same code but another children:
<ListItemButton
onClick={() => onClickResult(q)}
>
<HighlightText text={highlighted} />
</ListItemButton>
where Highlighted is defined before the return():
const HighlightText = ({ text }: { text: string }) => {
const splitText = text.split('}');
const hasHighlights = splitText.length > 1;
if (!hasHighlights) {
return <Typography variant="body1">{text}</Typography>;
}
return (
<StyledHighlightText>
<Typography variant="body1">{splitText[0].replace('{', '')}</Typography>
<StyledHighlightTextBold variant="body1">
{splitText[1]}
</StyledHighlightTextBold>
</StyledHighlightText>
);
};
and the on click:
const onClickResult = (query: string) => {
window.location.href = `${url}?q=${encodeURIComponent(
query,
)}&origin=PHRASE_SUGGEST`;
};
Now i have to click twice before the onClick gets fired. I see absolutely no reason for it to take 2 clicks instead of 1 now in order to trigger click
I found a workaround: create method instead of a component
<ListItemButton
onClick={() => onClickResult(q)}
>
{getHighlightText(highlighted)}
</ListItemButton>
and
const getHighlightText = (text: string) => {
const splitText = text.split('}');
const hasHighlights = splitText.length > 1;
if (!hasHighlights) {
return <Typography variant="body1">{text}</Typography>;
}
return (
<StyledHighlightText>
<Typography variant="body1">{splitText[0].replace('{', '')}</Typography>
<StyledHighlightTextBold variant="body1">
{splitText[1]}
</StyledHighlightTextBold>
</StyledHighlightText>
);
};
I still have no clue why using the component-way doesn't work

Mobx only re-render item when computed value changes

I have a list of medias and my goal is to be able to show the currently playing media.
To do so, I compare the playing media ID with the one from the list to apply the correct style.
My issue is that when clicking on another item, all items re-render because they have a dependency on the playing media which is observable.
class AppStore {
...
get playingVideo() {
if (!this.player.videoId || this.player.isStopped) {
return null;
}
return this.videos[this.player.videoId];
}
}
const DraggableMediaItem = observer(({ video, index }) => {
const store = useAppStore();
const isMediaActive = computed(
() => store.playingVideo && video.id === store.playingVideo.id
).get();
console.log("RENDER", video.id);
const onMediaClicked = (media) => {
if (!isMediaActive) {
playerAPI.playMedia(media.id).catch(snackBarHandler(store));
return;
}
playerAPI.pauseMedia().catch(snackBarHandler(store));
};
let activeMediaProps = {};
if (isMediaActive) {
activeMediaProps = {
autoFocus: true,
sx: { backgroundColor: "rgba(246,250,254,1)" },
};
}
return (
<Draggable draggableId={video.id} index={index}>
{(provided, snapshot) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
button
disableRipple
{...activeMediaProps}
onClick={() => onMediaClicked(video)}
>
<Stack direction="column" spacing={1} sx={{ width: "100%" }}>
<Stack direction="row" alignItems="center">
<ListItemAvatar>
<MediaAvatar video={video} />
</ListItemAvatar>
<ListItemText primary={video.title} />
<ListItemText
primary={durationToHMS(video.duration)}
sx={{
textAlign: "right",
minWidth: "max-content",
marginLeft: "8px",
}}
/>
</Stack>
</Stack>
</ListItem>
)}
</Draggable>
);
});
I thought making isMediaActive a computed value would prevent that, but since the value the computation is based on changes, it triggers an update.
Is it possible to only re-render when the computed value changes ?
[EDIT]
Following #danila's comment, I cleaned up my code and injected the isActive parameter. However, I must still be missing something, since the List doesn't re-render when the player's video changes.
That would be the current pseudocode:
const MediaItem = observer(({ isActive }) => {
let activeMediaProps = {};
if (isActive) {
activeMediaProps = {
sx: { backgroundColor: "rgba(246,250,254,1)" },
};
}
return <ListItem {...activeMediaProps}> ... </ListItem>;
});
const Playlist = observer(() => {
const store = useAppStore();
const items = store.playlist;
return (
<List>
{items.map((item) => (
<MediaItem isActive={item.id === store.player.videoId} />
))}
</List>
);
});
[EDIT 2]
Code sandbox link with a working example:
https://codesandbox.io/s/silent-lake-2lvdc?file=/src/App.js
Thank you in advance for your help and time.
First of all you can't use computed like that. In most cases computed should be used like a property in your store. Similar to observable.
As for the question, if you don't want items to rerender you could provide this flag through props, something like that in pseudocode
const List = observer(() => {
return (
<div>
{items.map(item => (
<Item isMediaActive={store.playingVideo && item.id === store.playingVideo.id} />
))}
</div>
)
})
It is also better to have that list as "standalone" component, don't just render items inside your whole view. More info here https://mobx.js.org/react-optimizations.html#render-lists-in-dedicated-components
EDIT:
There is also another way, which is actually "more MobX" way of doing things, is to have isPlaying flag in the item object itself. But that might require you to change how you work with your data, so the first example is probably easier if you have already setup everything else.
With flag on the item you don't even need to do anything else, you just check if it is active or not and MobX will do everything else. Only 2 items will rerender when you change the flag. The action in your store could look like that:
playItem(itemToPlay) {
this.items.find(item => item.isPlaying)?.isPlaying = false
itemToPlay.isPlaying = true
}

Trying to conditional render css to on alternating items in an array with useState in React with MUI

I have an object of arrays that I'm mapping over called featuredProj, and on alternate items, I want the CSS to conditionally render as not to repeat so much code. Using useState in the handleSide function I get the error too many rerenders. How can I solve this, or is there a better solution to rendering jsx while mapping over an array of objects.
const useStyles = makeStyles(() => ({
title: {
textAlign: (side) => (side ? "right" : "left"),
},
}));
const FeaturedProjects = () => {
const [side, setSide] = useState(true);
const classes = useStyles(side);
const handleSide = (project, index) => {
if (index === 0 || index % 2 === 0) {
// I tried setSide(false), setSide(prev => !prev)
return (
<Grid Container key={index}>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
);
} else {
return (
<Grid Container key={index}>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
);
}
};
return (
<Container>
{featuredProj.map((proj, ind) => (
<Reveal duration="2000" effect="fadeInUp">
{handleSide(proj, ind)}
</Reveal>
))}
</Container>
);
};
Thanks in advance for any assistance!
You cannot call setState() inside render method. setState() will trigger a re-rendering which calls render method again which leads to another setState().. you get the idea.
If you want it to work, you need to create separate component with the side props and pass it as an argument to your style hook.
const FeaturedProjects = () => {
const classes = useStyles(side);
return (
<Container>
{featuredProj.map((proj, ind) => (
<FeaturedProjectItem
key={index}
project={proj}
side={index === 0 || index % 2 === 0}
/>
))}
</Container>
);
};
const useStyles = makeStyles({
title: {
textAlign: (side) => (side ? "right" : "left"),
},
});
const FeaturedProjectItem = ({ side, project }) => {
const classes = useStyles(side);
return (
<Reveal duration="2000" effect="fadeInUp">
<Grid Container>
<Typography className={classes.title}>{project.title}</Typography>
</Grid>
</Reveal>
);
};

React - Communicate between Material UI Components

I'm using Material UI's nested/select ItemList component to dynamically generates any number of dropdown menu items based on how many items belong in this header as you can maybe tell from the mapping function. On another file 1 layer above this one, I am again mapping and generating multiple of these DropDownMenus, is it possible for these components to communicate to each other?
This is the file in question
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 330,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
}
}));
export default function DropDownMenu(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(true);
let unitName = props.unit[0];
let chapterList = props.unit.slice(1);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (index) => {
console.log("ItemClicked");
console.log(index);
setSelectedIndex(index);
};
const handleClick = () => {
setOpen(!open);
};
const selectMenuItem = (chapter, index) => {
props.chooseChapter(chapter)
handleListItemClick(index)
}
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem button
className={classes.selected}
selected={selectedIndex === index}
onClick={() => selectMenuItem(chapter, index)}
key={index}>
<ListItemText primary={chapter} />
</ListItem>
)
})
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
</ListSubheader>
}
className={classes.root}
>
<ListItem button onClick={handleClick}>
<ListItemText primary={unitName} />
{!open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={!open} timeout="auto" unmountOnExit>
<List component="div" disablePadding className={classes.selected}>
{dropDownUnit}
</List>
</Collapse>
</List>
);
}
Psudo Style - What I'm trying to accomplish
<DropDownMenu>
<MenuItem> // Suppose this is selected
<MenuItem>
<DropDownMenu>
<MenuItem> // onClick --> Select this and deselect all other selected buttons
You can have a parent of these components such that the parent will keep the state of who is active. That way you can pass that state & the state setter as props so that everyone will know who is active
export default function App() {
const [selectedItem, setSelectedItem] = React.useState();
return (
<>
<DropDownMenu
selectedItem={selectedItem} // pass down as props
setSelectedItem={setSelectedItem} // pass down as props
unit={...}
chooseChapter={function () {}}
/>
...
On the children, simply refactor the Call To Action (in this case onClick) to set the state using the passed down props. Pay attention to the selected prop of ListItem, we now use the state we have passed down from the parent
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem
button
className={classes.selected}
selected={props.selectedItem === chapter}
onClick={() => props.setSelectedItem(chapter)}
key={index}
>
<ListItemText primary={chapter} />
</ListItem>
);
});

Resources