Home > Blockchain >  Stop entire List from rerendering on selecting element
Stop entire List from rerendering on selecting element

Time:11-08

I am struggling to stop my entire List from rerendering. When I select an item it should be underlined. It should suffice if the newly selected and the previously selected elements are rerendered.
In a worst-case scenario, this List can get hundreds of entries long and it's getting really slow. So I tried using React.memo method, but I am probably using it wrong.
These are my custom Functions for List and ListEntry (changeSelected and currentlySelected are the state and setState functions from the parent)

function CustomList(props: any) {
    return (
            <List
                component="div"
            >
                {props.data.map((entryData: any, index: number) => {
                    return (
                        <React.Fragment key={index}>
                            <CustomListEntry
                                entryData={entryData}
                                changeSelected={props.changeSelected}
                                style={{
                                    borderBottom:
                                        props.currentlySelected == entryData.id ? "dashed" : "",
                                }}
                            />
                        </React.Fragment>
                    );
                })}
            </List>
        
    );
}
const CustomListEntry = React.memo((props: any) => {
    return (
        <ListItemButton
            style={props.style}
            onClick={() => {
                props.changeSelected(props.entryData.id);
            }}
        >
            <ListItemText>
                    {props.entryData.exampledata}
            </ListItemText>
        </ListItemButton>
    );
});

And the parent Component:

function Root(props:any){
const [selectedId, setSelectedId] = React.useState("");
   ...
    return{
                <CustomList
                    data={data}
                    changeSelected={setSelectedId}
                    currentlySelected={selectedId}
                ></CustomList>
    }
}

Edit: Here my full code, if its important for context. (useApiEnpoint just uses Axios to get the initial data for the List. Is just called on first mounting and shouldnt affect the poor performance on selection change)

export default function SubSiteDokumentation(props: any) {
    const StammdatenContext = React.useContext(ProbandContext);
    const [selectedId, setSelectedId] = React.useState("");
    const { formData, handleGetDetail, isLoading, isError } = useAPIEndpoint({
        endpoint: "KontaktetermineList",
        formName: "Termine",
        pk: StammdatenContext.currentPnr,
        enableSnackbar: true,
        initialLoading: true,
    });
    React.useEffect(() => {
        handleGetDetail();
    }, []);
    if (isError) {
        return (
            <Typography variant="h4" align="center" color="red">
                Termine konnten nicht geladen werden! Versuchen sie es später erneut
                oder wenden sie sich an den Administrator
            </Typography>
        );
    }
    if (isLoading) {
        return <Spinner animation="border"></Spinner>;
    }
    return (
        <Grid container spacing={2}>
            <Grid item xs={7}>
                <TermineList
                    data={formData}
                    changeSelected={setSelectedId}
                    currentlySelected={selectedId}
                ></TermineList>
            </Grid>
            {/* <Grid item xs={5}>
                <TerminDetail currentlySelected={selectedId}></TerminDetail>
            </Grid> */}
        </Grid>
    );
}


function TermineList(props: any) {
    return (
        <Box
            pl={"10px"}
            sx={{
                background: "#b4f5bf",
            }}
            className="my-sidebarAuftraege my-nice-scrollbarBox"
        >
            <ListSubheader sx={{ background: "#b4f5bf", zIndex: "1000" }}>
                <Typography align="center" variant="h5" color="black">
                    Dokumentation
                </Typography>
            </ListSubheader>
            <List
                component="div"
                disablePadding
                sx={{ background: "#e3ffe9", borderRadius: "10px" }}
            >
                {props.data.map((termin: any, index: number) => {
                    let style = {
                        backgroundColor: index % 2 === 1 ? "#00000011" : "",
                        borderColor: "#89ca07",
                        borderBottom: props.currentlySelected == termin.id ? "dashed" : "",
                    };
                    return (
                        <React.Fragment key={index}>
                            <TermineListEntry
                                termin={termin}
                                changeSelected={props.changeSelected}
                                style={style}
                            />
                            <Divider
                                sx={{ borderBottomWidth: 2, backgroundColor: "black" }}
                            ></Divider>
                        </React.Fragment>
                    );
                })}
                <ListItem
                    sx={{
                        display: "flex",
                        justifyContent: "center",
                        backgroundColor:
                            (props.data.length % 2 === 1 && "#00000011") || "#00000000",
                    }}
                >
                    {/* <Box sx={{ display: "flex", justifyContent: "center" }}>
                        <div>test</div>
                    </Box> */}
                </ListItem>
            </List>
        </Box>
    );
}

const TermineListEntry = React.memo((props: any) => {
    return (
        <ListItemButton
            style={props.style}
            onClick={() => {
                props.changeSelected(props.termin.id);
            }}
        >
            <ListItemText>
                <Grid container>
                    <Grid item xs={2}>
                        {Moment(props.termin.datum).format("DD.MM.YYYY")}
                    </Grid>
                    <Grid item xs={2}>
                        {props.termin.uhrzeit}
                    </Grid>
                    <Grid item xs={6}>
                        {props.termin.inhalt}
                    </Grid>
                    <Grid item xs={1}>
                        {props.termin.art}
                    </Grid>
                    <Grid item xs={1}>
                        {props.termin.kv}
                    </Grid>
                </Grid>
            </ListItemText>
        </ListItemButton>
    );
});
.my-sidebarAuftraege {
    height: calc(
        calc(calc(100vh - #{$navbar-height}) - #{$subNavbar-height}) - 24px
    );
    padding: 0px;

    border-style: solid;
    border-color: $primary-dark;
    background-color: #00000009;
    border-radius: 5px;

    overflow: scroll;
    overflow-x: hidden;

    -webkit-overflow-scrolling: touch;
}

//Scrollbar Styling for Edge

.my-nice-scrollbarBox::-webkit-scrollbar {
    width: 10px;
    height: 6px;
}
.my-nice-scrollbarBox::-webkit-scrollbar-track {
    background: transparent;
}
.my-nice-scrollbarBox::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
}
.my-nice-scrollbarBox::-webkit-scrollbar-thumb:hover {
    background: rgba(0, 0, 0, 0.4);
}
.my-nice-scrollbarBox::-webkit-scrollbar-thumb:active {
    background: rgba(0, 0, 0, 0.9);
}

//Scrollbar Styling for Firefox
.my-nice-scrollbarBox {
    scrollbar-width: thin;
    scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}

CodePudding user response:

The reason is simple : when you change the state on the parent level, the parent will rerender, forcing rerendering of all child elements.

Note that this should not be too long anyway so you probably have another performance issue somewhere :) (the DOM is not updated by react) - e.g. having console.log(..) somewhere

Here is a code pen with 10k buttons in the list, still you can see that the rendering is pretty quick: https://codepen.io/aSH-uncover/pen/rNKWaKE

for (let i = 0 ; i < 10000 ; i  ) {
  data.push({ id: String(i), exampledata: `Value ${i}`})
}

If you really need to get rid of the whole rendering you will have to take another approach such as a global store with one entry per button and stop storing the selectedId in the parent state :)

CodePudding user response:

You use React.memo correctly, but you need to modify CustomList a bit:

  • Shouldn't use index as a key, because when props.data changes, index may change as well that will make the entire list re-rendered
  • You don't need to wrap React.Fragment and move key to CustomListEntry element which would help to recognize element changes
  • style with {} may cause re-renderings, you should put the condition outside to prevent initializing new objects. Note that you can use className to customize styles which is a more common way
function CustomList(props: any) {
  return (
    <List component="div">
      {props.data.map((entryData: any) => (
        <CustomListEntry
          entryData={entryData}
          changeSelected={props.changeSelected}
          key={entryData.id}
          style={
            props.currentlySelected === entryData.id && {
              borderBottom: "dashed",
            }
          }
        />
      ))}
    </List>
  );
}
  • Related