React: prevent list from rerendering all elements on prop change - reactjs

I'm trying to recreate the effect shown at https://hexed.it/
When you hover over either list the corresponding byte in the other list is also highlighted. I figured a panel with each list inside it that had a state with the current hovered byte would do but it seems that React wants to re-render the entire list or do something strange every time resulting in larger files being unbearably slow.
I see a lot of "use memo! use the useCallback hook!" when searching and I've tried... it's still slow and I'm not sure why. It seems like it's only rendering the updated HexByte but it's still unacceptably slow for large files.
Sandbox: https://codesandbox.io/s/flamboyant-ellis-btfk5s
Can someone help me quicken/smooth out the hovering?

I solved it using this answer: Prevent DOM element re-render in virtualized tree component using react-window
In short the things I've learned:
memo has no effect if a component has a useState in it
Large lists of data should be rendered using a library like react-window
The cell rendering function as mentioned in the answer above can't be part of a parent component
As an example for anyone coming here, the new HexPanel class looks like so
import Box from '#mui/material/Box';
import { memo } from 'react';
import { FixedSizeGrid as Grid, areEqual } from 'react-window';
const HexByte = memo(function HexByte(props) {
const onMouseEnter = () => {
props.onHover(props.index);
//setInside(true);
}
const onMouseLeave = () => {
//setInside(false);
}
const onClick = () => {
//setClicked(true);
}
return (
<span
style={{
display: 'inline-block',
padding: '5px',
backgroundColor: props.hoverIndex == props.index ? '#777' : 'transparent',
color: 'darkblue'
}}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{props.byte}
</span>
)
}, (prevProps, nextProps) => nextProps.hoverIndex != nextProps.index);
const Cell = memo(function({ data, columnIndex, rowIndex, style }) {
return (
<div style={style}>
<HexByte byte={data.hex[rowIndex][columnIndex]} onHover={data.onHover} hoverIndex={data.hoverIndex} index={`${rowIndex}${columnIndex}`} />
</div>
)
}, areEqual);
const HexPanel = (props) => {
return (
<Box
sx={{
fontFamily: 'Source Code Pro',
display: 'flex',
flexDirection: 'column',
}}
>
<Grid
columnCount={16}
columnWidth={30}
height={900}
itemData={props}
rowCount={props.hex.length}
rowHeight={35}
width={500}
>
{Cell}
</Grid>
</Box>
)
}
export default HexPanel;

Related

Material UI Rating wrapping long values

I am currently trying to work around the Material UI rating component and how to do a flex-wrap if the icons overflow the width of the parent component.
If I try to add flex-wrap: wrap to the rating component, it actually wraps the icons but the interactive functionality stops working pas the first line.
Here is a code example below to better demonstrate this:
Code Example in CodeSandbox
Is there a way to make it work with flex-wrap? If anyone could help I will very much appreciate.
I have decided that was better to build one by myself with the ability to wrap if the max value is big.
Will leave it here so someone who might have the same issue as me can use it.
CustomRating.js
import React, { useState } from 'react'
import { Tooltip } from '#mui/material'
import './CustomRating.css'
function CustomRating({ max, value, onChange, icon, emptyIcon }) {
const [innerValue, setInnerValue] = useState(value)
const checkIfIconInsideValue = (index) => {
return value >= index + 1
}
const handleMouseHover = (e, index) => {
if (e.type === 'mouseenter') {
setInnerValue(index)
return
}
setInnerValue(value - 1)
}
return (
<Tooltip title={innerValue} placement='top'>
<div className='custom-rating-main-div'>
{Array.from({ length: max }).map((elem, index) => {
return (
<div
className={`custom-rating-icon-div ${checkIfIconInsideValue(index) ? 'filled' : ''}`}
key={index}
onClick={() => onChange(index + 1)}
onMouseEnter={(e) => handleMouseHover(e, index)}
onMouseLeave={(e) => handleMouseHover(e, index)}
>
{checkIfIconInsideValue(index) || innerValue >= index ? icon : emptyIcon}
</div>
)
})}
</div>
</Tooltip>
)
}
export default CustomRating
CustomRating.css
.custom-rating-main-div {
display: flex;
flex-wrap: wrap;
}
.custom-rating-icon-div {
cursor: pointer;
}
.custom-rating-icon-div.filled > svg {
fill: #61634f
}
.custom-rating-icon-div > svg {
fill: rgba(97, 99, 79, 0.5)
}
.custom-rating-icon-div:hover > svg {
fill: #61634f;
transform: scale(1.2);
}
As you may notice this is specific to my problem but can be very easily adapted to any case.
keep in mind that this is very rough and can be updated to better follow conventions and for better performance, but for now it is my solution

React useEffect only works if I make a change to the code once the page is open

I'm having a problem in React where the code I have for my useEffect in a component works, but only if I manually make a change to the code (not even a substantial one, it can be as small as adding or removing a console.log) after loading the component. Here's a link to a screen recording of what happens.
I am using the Material-UI Autocomplete component to create a list, but it's like the useEffect won't actually kick in when I load the page with the component until I make some sort of change to the code after the component loads (again: even something as small as commenting out or adding a console.log works). Once I make that change, the list will appear. But if I navigate away from the page or reload, I'm back to square 1 again.
Similarly, when I try to select an option in the list, it looks as if it didn't work until I make a change to the code. Only then will the selection change I made to the list appear.
Here's the code I have for that component:
import React, { useState, useEffect } from 'react'
import { makeStyles } from '#material-ui/core/styles'
import Checkbox from '#material-ui/core/Checkbox'
import TextField from '#material-ui/core/TextField'
import Autocomplete from '#material-ui/lab/Autocomplete'
import Typography from '#material-ui/core/Typography'
import CheckBoxOutlineBlankIcon from '#material-ui/icons/CheckBoxOutlineBlank'
import CheckBoxIcon from '#material-ui/icons/CheckBox'
const icon = <CheckBoxOutlineBlankIcon fontSize='small' />
const checkedIcon = <CheckBoxIcon fontSize='small' />
const useStyles = makeStyles((theme) => ({
root: {
margin: 'auto'
},
paper: {
width: 250,
height: 230,
overflow: 'auto',
backgroundColor: '#bdbdbd'
},
button: {
margin: theme.spacing(0.5, 0)
},
checkbox: {
color: '#FFF342'
},
textfield: {
backgroundColor: '#bdbdbd'
}
}))
const WordSelect = ({ newListState, setNewListState }) => {
// const WordSelect = ({ newListState, setNewListState }) => {
const classes = useStyles()
const [wordsText, setWordsText] = useState({
wordOptions: []
})
useEffect(() => {
const wordsText = []
newListState.words.map(x => {
wordsText.push(x.text)
})
// console.log(wordsText)
setWordsText({ ...wordsText, wordOptions: wordsText })
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<Autocomplete
multiple
id='checkboxes-tags-demo'
disableCloseOnSelect
value={newListState.selected}
options={wordsText.wordOptions}
getOptionLabel={(option) => option}
getOptionSelected={(option, value) => option === value}
onChange={(event, newValue) => {
newListState.selected = newValue
}}
renderOption={(option, { selected }) => (
<>
<Checkbox
className={classes.checkbox}
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
<Typography className={classes.checkbox}>
{option}
</Typography>
</>
)}
style={{ width: 500 }}
renderInput={(params) => (
<TextField {...params} className={classes.textfield} variant='outlined' placeholder='Select Words' />
)}
/>
</>
)
}
export default WordSelect
I'm really not sure what's going on. It seems like the code must be right since the types of changes I make to get it to show up should have no effect on what renders on the page, but then why won't it render from the start? Any tips appreciated.
You have not added newListState to your useEffect dependency array. Therefore, useEffect runs only on the first render of the component. When you do a change in code, live reload reloads the component and the useEffect hook runs.
Try this:
useEffect(() => {
const wordsText = []
newListState.words.map(x => {
wordsText.push(x.text)
})
// console.log(wordsText)
setWordsText({ ...wordsText, wordOptions: wordsText })
}, [setWordsText, newListState])
You really shouldn’t be using ‘ // eslint-disable-line react-hooks/exhaustive-deps’ as it helps catch problems like these. It also might help you catch some nasty bugs due to stale data or functions.

React: Slider with useEffect hook not working properly when trying to save the slider's state into local storage

I'm trying to save the state of my slider in local storage using the UseEffect hook, so the selected value in the slider doesn't get lost when the user refreshes the page. However, the slider is acting weirdly when I add this functionality.
Here is the code in the parent component:
function ParentComponent() {
const [prefState, setPrefState] = React.useState(50);
React.useEffect(() => {
const data = window.localStorage.getItem("pref_state");
if (data) {
setPrefState(JSON.parse(data));
}
}, []);
React.useEffect(() => {
window.localStorage.setItem("pref_state", JSON.stringify(prefState));
});
const handlePrefSliderChange = (event, new_value) => {
setPrefState(new_value);
};
return (
<div className={classes.root}>
<Grid item xs={12} >
<CustomizedPrefSlider
PrefSliderValues={prefState}
onPrefSliderChange={handlePrefSliderChange}
/>
</Grid>
</div>
);
}
export default ParentComponent;
Here is the code for the child component:
const PrefSlider = withStyles({
root: {
color: '#52af77',
height: 8,
padding: '13px 0',
},
track: {
height: 8,
},
rail: {
color: '#d8d8d8',
opacity: 1,
height: 8,
},
})(Slider);
export default function CustomizedPrefSlider({PrefSliderValues, onPrefSliderChange}) {
const classes = useStyles();
return (
<div>
<div className={classes.title}>
<Typography className={classes.titleFont}>
How important is this feature to you?
</Typography>
</div>
<div className={classes.root}>
<PrefSlider
aria-labelledby="discrete-slider"
value={PrefSliderValues}
onChange={onPrefSliderChange}
min={0}
max={100}
step={25}
/>
</div>
</div>
);
}
Is this the correct way of saving the state of the slider to avoid losing data when the user refreshes the page? Why am I missing some of the stylings that I added to my slider when I add this functionality?
Thanks in advance!
Doing it this way looks like it should technically work, but seems very inefficient.
I'd suggest using a custom made hook, something like useLocalStorage or #rehooks/local-storage
useLocalStorage is nice because the code is small and easy to understand, and it would shrink your code down to this:
const [prefState, setPrefState] = useLocalStorage(50);

React-bootstrap cards not wrapping

My Code is:
import React, { useEffect, useState } from "react";
import styles from "./Cards.module.css";
import { CardDeck, Card } from "react-bootstrap";
const Cards = ({ animeArray }) => {
const [aanimeArray, setAnimeArray] = useState([]);
useEffect(() => {
setAnimeArray(animeArray);
}, [animeArray]);
if (!aanimeArray) {
return;
}
console.log("Anime Array", aanimeArray);
return (
<div className={styles.container}>
{aanimeArray === [] ? (
<h1>Search</h1>
) : (
<CardDeck>
{aanimeArray.map((anime) => {
return (
<Card>
<Card.Img variant = "top" src={anime.image_url} />
<Card.Body>
<Card.Title>{anime.title}</Card.Title>
</Card.Body>
<Card.Footer>
<small className="text-muted">{anime.rated}</small>
</Card.Footer>
</Card>
);
})}
</CardDeck>
)}
</div>
);
};
export default Cards;
I am not using any custom styling whatsoever.
The result of the above mentioned code is as seen on this image:
Image of the issue
You have to make the effort of making them wrap. In fact, as seen on the documentation, majority of the developers' examples includes the CSS property width with a value of 18rem.
Here is an example by leveraging minWidth:
const sampleStyle = {
minWidth: "20%",
flexGrow: 0
};
<Card style={sampleStyle}>
First thing.
aanimeArray === []
won't work since you are comparing an array with another array.
Best way in Javascript for this is to check the length of the array.
aanimeArray.length === 0
means it is an empty array.
About the styling I think you need to show us the CSS code as well. I'm not sure what CardDeck component does...

Call to setState in onDragStart causes DOM node to be deleted?

I use a list of styled components for displaying some info. I want this info to be sortable. The real problem I'm trying to solve is actually way more complex than what I'm demonstrating here. So any odd design choices are very specific to what I'm trying to do. I'm just mentioning it because the code I'm showing will be very simplified but it will also show some of these at first glance odd design choices.
I've read this article: https://medium.com/the-andela-way/react-drag-and-drop-7411d14894b9
Temitope Emmanuel (the author) did what I'm trying to achieve but with just a plain div. I don't know whether he tested all of what he proposes in his article.
Off to some code:
import React, { Component, Fragment } from 'react';
import styled from 'styled-components';
export default class SomeList extends Component {
constructor(props) {
super(props);
// in real problem all of these are props
// pulled off the state of a parent
this.state = {
dragging: false,
listOfChildrenInOrder: ['1', '2', '3'],
itemComponent: styled.div`
border: 1px solid black;
`,
};
}
render() {
const {
dragging,
listOfChildrenInOrder,
itemComponent: ItemComponent,
} = this.state;
const {
children,
} = this.props;
const Container = styled.div`
display: grid;
grid-template-rows: max-content;
grid-template-columns: repeat(${listOfChildrenInOrder.length}, max-content) 1fr;
`;
const Droppable = styled.div`
&:hover {
background-color: rgba(0,0,0,0.4);
}
`;
return (
<Container>
<Fragment>
{listOfChildrenInOrder.map(((cid, i) => (
<ItemComponent
draggable
key={`ic-${cid}`}
style={{
gridArea: `1 / ${i + 1} / span 1 / span 1`,
}}
onDragStart={(e) => {
this.setState({ dragging: true });
e.dataTransfer.setData('text/plain', `${cid}`);
}}
onDragEnd={() => {
this.setState({ dragging: false });
// doesn't even fire anymore
}}
>
{children.find(c => c.key === cid)}
</ItemComponent>
)))}
</Fragment>
<Fragment>
{dragging && listOfChildrenInOrder.map(((cid, i) => (
<Droppable
key={`d-${cid}`}
style={{
gridArea: `1 / ${i + 1} / span 1 / span 1`,
}}
onDragOver={(e) => {
e.stopPropagation();
e.preventDefault();
}}
onDrop={() => {
// do whatever (out of scope), doesn't get called anyway
}}
>
{children.find(c => c.key === cid)}
</Droppable>
)))}
</Fragment>
</Container>
);
}
}
I'm expecting the reconciler (Fiber) to update the DOM node without straight out replacing it in the middle of a drag operation. I'm using these things to act as highlighters. The real Problem I'm trying to solve actually makes a difference on where exactly stuff gets dropped, so the grid in the real problem is finer, with more droppables and one item component spaning multiple grid columns. Like I said: odd choices, but not without purpose.
Okay, I know now what was causing this whole operation to fail. The reason was dynamically creating new styled components in the render loop. Never do that. Just another rule of thumb learned.

Resources