Prevent checkboxes from re-rendering in React - javascript

I'm trying to render multiple checkboxes based on dynamic return data and have their checked status stored in a local state.
However the performance starts to degrade when higher number of checkboxes are generated. I noticed the issue is due to the constant re-rendering of ALL the checkboxes whenever any one of them is checked (checkbox states are all stored in the same object with different keys)
Here is my sample code and a codesandbox link to see the actual performance issue (notice the delay when a checkbox is selected)
export default function App() {
const [checkboxResponse, setCheckboxResponse] = useState([]);
const [checkboxList, setCheckboxList] = useState();
const [checkboxStates, setCheckboxStates] = useState({});
useEffect(() => {
//Generate dummy data
const dummyData = [];
for (let i = 0; i < 1000; i++) {
dummyData.push(i.toString());
}
//Set dummyData as checkbox dynamic data
setCheckboxResponse(dummyData);
}, []);
useEffect(() => {
//When checkbox is clicked, add to local checkbox states object
const checkboxChange = key => event => {
setCheckboxStates({ ...checkboxStates, [key]: event.target.checked });
};
//Check if data is available
if (checkboxResponse) {
const checkboxes = checkboxResponse.map(key => {
const value = checkboxStates[key] ? checkboxStates[key] : false;
//Render checkbox
return (
<FormControlLabel
key={key}
checked={value}
control={
<Checkbox
size="small"
value={key}
onChange={checkboxChange(key)}
/>
}
label={key}
/>
);
});
setCheckboxList(checkboxes);
}
}, [checkboxResponse, checkboxStates]);
return (
<div className="App">
{checkboxList}
</div>
);
}
CodeSandbox
It seems that whenever checkboxStates is changed, the useEffect hook is re-run, triggering a re-render of all the checkboxes again.
Is it possible to prevent React from re-rendering all the checkboxes again whenever the state is changed? Or do we have to create a separate state for every single checkbox dynamically?
Any help would be greatly appreciated.

You can use React.memo to prevent re-render of unchanged check-boxes. Like this:
import React, { useState, useEffect } from "react";
import { Checkbox, FormControlLabel } from "#material-ui/core";
import "./styles.css";
export default function App() {
const [checkboxResponse, setCheckboxResponse] = useState([]);
const [checkboxStates, setCheckboxStates] = useState({});
//When checkbox is clicked, add to local checkbox states object
const checkboxChange = key => event => {
setCheckboxStates({ ...checkboxStates, [key]: event.target.checked });
};
useEffect(() => {
//Generate dummy data
const dummyData = [];
for (let i = 0; i < 1000; i++) {
dummyData.push(i.toString());
}
//Set dummyData as checkbox dynamic data
setCheckboxResponse(dummyData);
}, []);
return (
<div className="App">
{checkboxResponse.map(key => {
const value = checkboxStates[key] ? checkboxStates[key] : false;
//Render checkbox
return (
<FormControlLabel
key={key}
checked={value}
control={<MemoCheckbox key={key} checkboxChange={checkboxChange} />}
label={key}
/>
);
})}
</div>
);
}
const CustomCheckbox = ({ key, checkboxChange }) => (
<Checkbox size="small" value={key} onChange={checkboxChange(key)} />
);
const MemoCheckbox = React.memo(
CustomCheckbox,
(prev, next) => prev.key === next.key
);
However, it might still be not fast enough as when you click the checkbox it still loops trough .map and creates elements.
Here is docs reference for Memo

Related

How to change checkbox state for one element instead of all

I am trying to change the state of a checkbox when I have two, but all checkboxes are being checked at the same time, I tried different solutions for 5 days and still nothing ... I'm quite new to react so I'm lost.
import React, { ChangeEvent, useCallback, useState } from 'react';
import ReactDOM from 'react-dom';
import { Checkbox, Pane } from 'evergreen-ui';
function ControlledCheckboxExample() {
const [checkedItems, setCheckedItems] = React.useState(false)
const handleButtonClick = (e) => {
console.log(!checkedItems, e);
setCheckedItems(!checkedItems);
};
return (
<Pane>
<Checkbox
label="Controlled usage"
name="afaf"
key={1}
checked={checkedItems}
onChange={handleButtonClick.bind(name, 1)}
/>
<Checkbox
label="Controlled usage"
name="afatrf"
key={2}
checked={checkedItems}
onChange={handleButtonClick.bind(name, 2)}
/>
</Pane>
)
}
ReactDOM.render(
<ControlledCheckboxExample />,
document.getElementById("root")
)
This is my code, is there any solution you can propose?
Issue
The code is using and updating a single state for all checkbox inputs.
Solution
Convert the checkedItems to an object of booleans and use the onChange event object and the input name to toggle a specific input.
Example:
function ControlledCheckboxExample() {
const [checkedItems, setCheckedItems] = React.useState({});
const handleButtonClick = (e) => {
const { name } = e.target;
setCheckedItems(checkedItems => ({
...checkedItems,
[name]: !checkedItems[name]
}));
};
return (
<Pane>
<Checkbox
label="Controlled usage"
name="afaf"
key={1}
checked={checkedItems["afaf"]}
onChange={handleButtonClick}
/>
<Checkbox
label="Controlled usage"
name="afatrf"
key={2}
checked={checkedItems["afatrf"]}
onChange={handleButtonClick}
/>
</Pane>
);
}
You are using same state variable for both checkboxes and of course if you click on one the second will be set too.
Create another state variable for another checkbox or use an array like so
const [state, setState] = React.useState(new Array({length of how much boxes you have}).fill(false);
and then update state
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
}

How to prevent useState from rerendering the component again?

import { nanoid } from 'nanoid';
import React from 'react';
export default function Question(props) {
const answers = [...props.incorrect_answers, props.correct_answer];
const [selectedAnswer, setSelectedAnswer] = React.useState();
function handleChange(event) {
setSelectedAnswer(event.target.value);
console.log(event.target.value);
}
// Randomize answer of the Question
function createRandom(arr) {
let myArr = [...arr];
let randomizedArr = [];
while (myArr.length > 0) {
let randomIndex = Math.floor(Math.random() * myArr.length);
randomizedArr.push(myArr[randomIndex]);
myArr.splice(randomIndex, 1);
}
return randomizedArr;
}
const randomizedArr = createRandom(answers);
// RadioButton for the answers
const RadioButton = ({ label, value, onChange }) => {
return (
<label>
<input
type="radio"
name="answers"
value={value}
checked={selectedAnswer === value}
onChange={onChange}
/>
{label}
</label>
);
};
const answersElements = randomizedArr.map((answer) => {
return (
<RadioButton
key={nanoid()}
label={answer}
value={answer}
onChange={handleChange}
/>
);
});
return (
<div className="question-row">
<div className="single-question">{props.question}</div>
<div className="answers">{answersElements}</div>
</div>
);
}
When I select a radiobutton answer , React rerender the RaidoButton component and call my Randomize function which randomize the answers.
I do not know how to prevent Randomize function from being called after the first time.
in the two pictures when I select an answer for the first question , the answers get randomized again , I want to prevent this behavior
This is an XY question.
You need the component to rerender when the state changes (if for no other reason than to update which radio button is checked).
The problem is that you need the randomisation function to run only on the initial load.
Create a state for the randomized array:
const [randomizedArr, setRandomizedArr] = useState([]);
Then populate it in an effect hook with an empty dependency array (so it only runs on the initial load of the component).
useEffect(() => {
const randomizedArr = createRandom(answers);
setRandomizedArr(randomizedArr);
}, []);
const [randomizedArr, setRandomizedArr] = React.useState([]);
const [selectedAnswer, setSelectedAnswer] = React.useState();
React.useEffect(() => {
const answers = [...props.incorrect_answers, props.correct_answer];
const randomizedArr = createRandom(answers);
setRandomizedArr(randomizedArr);
}, [props.incorrect_answers, props.correct_answer]);

Manage checkbox state in an infinite scrolling table using React

Please see this codesandbox.
This codesandbox simulates a problem I am encountering in my production application.
I have an infinite scrolling table that includes checkboxes, and I need to manage the every-growing list of checkboxes and their state (checked vs non-checked). The checkboxes are rendered via vanilla functions (see getCheckbox) that render the React components. However, my checkboxes do not seem to be maintaining the parent state (called state in the code) and clicking a checkbox does not work. What do I need to do to make sure that clicking a checkbox updates state and that all of the checkboxes listen to state? Thanks! Code is also below:
index.js:
import ReactDOM from "react-dom";
import "#elastic/eui/dist/eui_theme_amsterdam_light.css";
import React, { useState, useEffect } from "react";
import { EuiCheckbox, htmlIdGenerator } from "#elastic/eui";
import { arrayRange, getState } from "./utils";
const Checkbox = ({ id, isChecked, onClick }) => (
<div style={{ margin: "1rem" }}>
<EuiCheckbox
id={htmlIdGenerator()()}
label={isChecked ? `${id} - On` : `${id} - Off`}
checked={isChecked}
onChange={() => onClick()}
/>
</div>
);
const getCheckbox = (props) => <Checkbox {...props} />;
const App = () => {
const [state, setState] = useState(getState(0, 1));
const [checkboxes, setCheckboxes] = useState([]);
const [addMoreCheckboxes, setAddMoreCheckboxes] = useState(true);
useEffect(() => {
if (addMoreCheckboxes) {
setAddMoreCheckboxes(false);
setTimeout(() => {
setState((prevState) => ({
...prevState,
...getState(checkboxes.length, checkboxes.length + 1)
}));
const finalCheckboxes = [...checkboxes].concat(
arrayRange(checkboxes.length, checkboxes.length + 1).map((id) =>
getCheckbox({
id,
isChecked: state[id],
onClick: () => handleClick(id)
})
)
);
setCheckboxes(finalCheckboxes);
setAddMoreCheckboxes(true);
}, 3000);
}
}, [addMoreCheckboxes]);
const handleClick = (id) =>
setState((prevState) => ({
...prevState,
[id]: !prevState[id]
}));
return <div style={{ margin: "5rem" }}>{checkboxes}</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
utils.js:
export const arrayRange = (start, end) =>
Array(end - start + 1)
.fill(null)
.map((_, index) => start + index);
export const getState = (start, end) => {
const state = {};
arrayRange(start, end).forEach((index) => {
state[index] = false;
});
return state;
};
There is one main reason your checkboxes are not updating.
It's your checkboxes state variable
This variable does not contain the data, but rather contains the processed React JSX elements themselves.
Not a wrong practice, but is uncommon. Kind of makes it easy to lose track of where data is actually stored, etc. I'd recommend using useMemo for UI-related memoization instead.
Related to previous point. Observe how in useEffect, when you're trying to append new data to the checkboxes variable, you're using the spread operator from the previous checkboxes value
Since the checkboxes value are simply processed JSX, they aren't re-rendered or re-processed (they are simply "there", like drawn pictures! No relations to the state at all!)
So basically, this operation is just adding new "drawings" of unchecked checkboxes to a previous list of drawn JSXes. So you just get a longer list of immutable, pre-rendered JSX checkboxes stuck in the unchecked state!
So.. to fix this I'd recommend that you separate data state from UI drawings. And maybe use useMemo to help.
I minimally modified your code, and it's located here (https://codesandbox.io/s/cranky-firefly-9ibgr). This should behave like you expect.
Here's the same exact modified code as in the sandbox for convenience.
import ReactDOM from "react-dom";
import "#elastic/eui/dist/eui_theme_amsterdam_light.css";
import React, { useState, useEffect, useMemo } from "react";
import { EuiCheckbox, htmlIdGenerator } from "#elastic/eui";
import { arrayRange, getState } from "./utils";
const Checkbox = ({ id, isChecked, onClick }) => (
<div style={{ margin: "1rem" }}>
<EuiCheckbox
id={htmlIdGenerator()()}
label={isChecked ? `${id} - On` : `${id} - Off`}
checked={isChecked}
onChange={() => onClick()}
/>
</div>
);
const getCheckbox = (props, key) => <Checkbox {...props} key={key}/>;
const App = () => {
// The checkbox check/uncheck state variable
const [state, setState] = useState(getState(0, 1));
// Checkboxes now just contains the `id` of each checkbox (purely data)
const [checkboxes, setCheckboxes] = useState([]);
const [addMoreCheckboxes, setAddMoreCheckboxes] = useState(true);
useEffect(() => {
if (addMoreCheckboxes) {
setAddMoreCheckboxes(false);
setTimeout(() => {
setState((prevState) => ({
...prevState,
...getState(checkboxes.length, checkboxes.length + 1)
}));
// Add new ids to the list
const finalCheckboxes = [...checkboxes].concat(
arrayRange(checkboxes.length, checkboxes.length + 1)
);
setCheckboxes(finalCheckboxes);
setAddMoreCheckboxes(true);
}, 3000);
}
}, [addMoreCheckboxes]);
const handleClick = (id) => {
setState((prevState) => ({
...prevState,
[id]: !prevState[id]
}));
};
// use useMemo to check for rerenders. (kind of like, data-driven UI)
const renderedCheckboxes = useMemo(() => {
return checkboxes.map((id) => {
// I'm adding a second argument to `getCheckbox` for its key
// This is because `React` lists (arrays of JSXs) need keys on each JSX components
// You could also just skip the function calling altogether
// and create the <Checkbox .../> here. There's little performance penalty
return getCheckbox({
id,
isChecked: state[id],
onClick: () => handleClick(id)
}, id)
});
}, [checkboxes, state]);
return <div style={{ margin: "5rem" }}>{renderedCheckboxes}</div>;
};
ReactDOM.render(<App />, document.getElementById("root"));
P.S. I agree with Oliver's comment that using useEffect with another toggle (addMoreCheckboxes) is quite unorthodox. I would suggest refactoring it to setInterval (or maybe a setTimeout with a boolean condition in a useEffect, in case you want more control).
The main problem here is that checkboxes is not directly dependent on state (the only time a checkbox is related to state is when a it is initialised with isChecked: state[id]).
This means that even though your state variable updates correctly when a checkbox is clicked, this will not be reflected on the checkbox itself.
The quickest fix here would be to amend the JSX returned by your component so as to directly infer the isChecked property for the checkboxes from the current state:
const App = () => {
// [...]
return <div style={{ margin: "5rem" }}>
{Object.keys(state).map((id) => getCheckbox({
id,
isChecked: state[id],
onClick: () => handleClick(id),
}))
}
</div>;
};
You may however notice now that your checkboxes state variable is becoming rather unnecessary (state being sufficient and holding all the necessary information for rendering all the right checkboxes). So you could consider rewriting your logic without the redundant checkboxes state variable.
As a side note, you are using useEffect() in combination with the addMoreCheckboxes state variable as a kind of timer here. You could simplify that portion of the code through the use of the probably more appropriate setInterval()

How to create a react native state with key and value array?

I've a flatlist with a list of options and a checkboxes in each row, and I need a state for the checkboxex, using hooks. I tought create a key-value relationship, an associative array somehow, so I can access to the proper state using the "item.option" as key:
export default function Questions({ navigation, route }) {
const [questionNumber, setQuestionNumber] = useState(0);
const data = route.params.data;
const numberOfQuestions = Object.keys(data.questions).length;
const [selectedOption, setSelectedOption] = useState([null]);
const [toggleCheckBox, setToggleCheckBox] = useState([false])
[...]
const renderItemMultipleChoice = ({ item }) => {
console.log(toggleCheckBox[item.option],item.option); //******here I get undefined
return (
<View style={[styles.option]}>
<CheckBox style={styles.checkBox}
disabled={false}
value={toggleCheckBox[item.option]}
onValueChange={(newValue) => multipleChoiceHandler(item.option, newValue)}
/>
<Text style={styles.optionText}>{item.option}</Text>
</View>
);
};
const multipleChoiceHandler = (item, newValue) => {
var newHash = toggleCheckBox
newHash[item] = newValue;
setToggleCheckBox({toggleCheckBox: newHash});
}
useEffect(() => {
if (data.questions[questionNumber].type_option != "open_choice") {
for (i = 0; i < Object.keys(data.questions[questionNumber].options).length; i++) {
var newHash = toggleCheckBox
newHash[data.questions[questionNumber].options[i].option] = false;
//*******every checkbox is false at the beginning...
setToggleCheckBox({toggleCheckBox: newHash});
console.log("toggle checkbox:",toggleCheckBox[data.questions[questionNumber].options[i].option],
data.questions[questionNumber].options[i].option); //****** here I get all false, the value I setted.
}
setSelectedOption([null]);
}
}, [questionNumber])
return(
<FlatList style={styles.flatlistOption}
data={data.questions[questionNumber].options}
renderItem={renderItemMultipleChoice}
keyExtractor={(item) => item.option}
/>
)
}
I'm supernoob about react, so to insert the intial state of toggleCheckBox for each element (using the parameter option to refer to the proper array element), I've used a for cycle... I know it's not proper and quite spaghetti code. Btw it should work, but when I try to access from the checklist to the toggleCheckBox state I get a undefined, so the checkbox state doesn't work properly. I don't know what I'm missing...

How to use React component's custom hook with "map"

I'm trying to make a Checkbox component.
Here is my Checkbox.tsx.
import React from "react";
import * as S from "./style";
const Checkbox: React.FC<S.ICheckboxProps> = ({ checked, setChecked }) => {
return <S.StyledCheckbox checked={checked} onClick={setChecked} />;
};
and this is my useCheckbox.tsx,
import { useState } from "react";
export const useCheckbox = (initialState: boolean) => {
const [checked, _setChecked] = useState<boolean>(initialState);
const setCheckedToggle = () => _setChecked((prev) => !prev);
const setCheckedTrue = () => _setChecked(true);
const setCheckedFalse = () => _setChecked(false);
return { checked, setCheckedToggle, setCheckedTrue, setCheckedFalse };
};
export default Checkbox;
It works good. I can use this like
import Layout from "components/Layout";
import { useCheckbox } from "hooks/useCheckbox";
import Checkbox from "components/Checkbox";
const Home = () => {
const { checked, setCheckedToggle } = useCheckbox(false);
return (
<Layout>
<Checkbox checked={checked} setChecked={setCheckedToggle} />
</Layout>
);
};
export default Home;
But I have trouble in the List component.
List has a Checkbox component, and I have to use this List with data.
const Home = ({data}) => {
return (
<Layout>
{data.map((d) => <List />)}
</Layout>
);
};
In this case, is there a way to determine if the list is selected?
If the List has useCheckbox, the Home component doesn't know the checked state.
Should I use useCheckbox in the Home component for data.length times? I think this is not good.
Thanks for reading, and Happy new year.
If you want the checkbox state to exist at the level of Home then you'll need state in the Home component that can handle multiple items, either as an array or object.
Then where you map over data you can pass down checked and setChecked as props to List, with all the logic defined in Home using the item index (or preferably an ID if you have one) in relation to your Home state.
Here's an example of a hook you could use in Home
import { useState } from "react";
export const useCheckboxes = () => {
const [checkedIds, setCheckedIds] = useState([]);
const addToChecked = (id) => setCheckedIds((prev) => [...prev, id]);
const removeFromChecked = (id) =>
setCheckedIds((prev) => prev.filter((existingId) => existingId !== id));
const isChecked = (id) => !!checkedIds.find(id);
const toggleChecked = (id) =>
isChecked(id) ? removeFromChecked(id) : addToChecked(id);
return { isChecked, toggleChecked };
};
And you would use it like this
const Home = ({ data }) => {
const { isChecked, toggleChecked } = useCheckboxes();
return (
<Layout>
{data.map((d) => (
<List
key={d.id}
checked={isChecked(d.id)}
toggleChecked={() => toggleChecked(d.id)}
/>
))}
</Layout>
);
};

Categories

Resources