How to change checkbox state for one element instead of all - javascript

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);
}

Related

Reactjs not calling onChange callback in child

I have written a re-usable input component for url if a url dont start with http then it will be added http in the beginning.
Here you go for the componet
import React, {useContext, useCallback} from 'react';
const InputURL = ({ name, onChange, ...rest}) => {
const sanitizeURLonChange = React.useCallback((value, actionMeta) => {
if (value.target.value) {
if (!value.target.value.startsWith('http')) {
value.target.value = 'http://' + value.target.value
}
}
}, [onChange])
return (
<>
<input
name={name}
{...rest}
onChange={sanitizeURLonChange}
/>
</>
);
}
export default InputURL;
But when i try to use it in my some component, the onChange doesn't work
I try this way
<InputURL onChange={(e) => console.log(e.target.value)} />
unfortunately the inputURL onChange not working anymore, can you please help me in this case?
I want to achieve. if user input url without http, it will add http,
Like i input it stackoverflow.com/ and then it will return https://stackoverflow.com/ in Onchange
You are closing the bracket right after the event argument : {(e)}. Try like this:
<inputURL onChange={(e, val) => console.log(val)} />
also you have to use the onChange you're passing as props:
const sanitizeURLonChange = (e, actionMeta) => {
let newValue = e.target.value
if (newValue) {
if (!newValue.startsWith('http')) {
newValue = "http://" + newValue
}
}
setVal(newValue);
onChange(event, newValue)
}
but it seems anyway the onChange you are passing as a props to inputURL is not used anywhere so I am not sure what you want to achieve. Also you are calling the component inputURL instead of InputURL and first letter uppercase is very important in JSX.
I think your problem is here:
value.target.value = 'http://' + value.target.value
You are trying to update input value by not using an hook.
Try to rewrite your code in this way:
import React, { useState } from 'react';
const InputURL = ({ name, onChange, ...rest}) => {
const [val, setVal] = useState("");
const sanitizeURLonChange = (value, actionMeta) => {
if (value.target.value) {
if (!value.target.value.startsWith('http')) {
setVal('http://' + value.target.value);
}
else setVal(value.target.value);
}
}
return (
<>
<input
name={name}
{...rest}
value={val}
onChange={sanitizeURLonChange}
/>
</>
);
}
export default InputURL;
Here codesandbox working example.

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 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>
);
};

Prevent checkboxes from re-rendering in React

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

Usage of useCallback and setting new object state using previous state as argument

Consider this basic form fields component with a custom form hook to handle input changes:
import React, { useState, useCallback } from 'react';
const useFormInputs = (initialState = {})=> {
const [values, setValues] = useState(initialState);
const handleChange = useCallback(({ target: { name, value } }) => {
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const resetFields = useCallback(() =>
setValues(initialState), [initialState]);
return [values, handleChange, resetFields];
};
const formFields = [
{ name: 'text', placeholder: 'Enter text...', type: 'text', text: 'Text' },
{ name: 'amount', placeholder: 'Enter Amount...', type: 'number',
text: 'Amount (negative - expense, positive - income)' }
];
export const AddTransaction = () => {
const [values, handleChange, resetFields] = useFormInputs({
text: '', amount: ''
});
return <>
<h3>Add new transaction</h3>
<form>
{formFields.map(({ text, name, ...attributes }) => {
const inputProps = { ...attributes, name };
return <div key={name} className="form-control">
<label htmlFor={name}>{text}</label>
<input {...inputProps} value={values[name]}
onChange={handleChange} />
</div>;
})}
<button className="btn">Add transaction</button>
</form>
<button className="btn" onClick={resetFields}>Reset fields</button>
</>;
};
Is there really any reason / advantage for me to use useCallback to cache the function in my custom hook? I read the docs, but I just coudln't grasp the idea behind this usage of useCallback. How exactly it memoizes the function between renders? How exactly does ti work, and should I use it?
Inside the same custom hook, you can see the new values state being updated by spreading the previous state and creating a new object like so: setValues(prev => ({ ...prev, [name]: value }));
Would there be any difference if I did this instead? setValues({ ...prev, [name]: value })
as far as I can tell, doesn't look like it has any difference right? I am simply accessing the state directly.. Am I wrong?
Your first question:
In your case it doesn't matter because everything is rendered in the same component. If you have a list of things that get an event handler then useCallback can save you some renders.
In the example below the first 2 items are rendered with an onClick that is re created every time App re renders. This will not only cause the Items to re render it will also cause virtual DOM compare to fail and React will re create the Itms in the DOM (expensive operation).
The last 2 items get an onClick that is created when App mounts and not re created when App re renders so they will never re render.
const { useState, useCallback, useRef, memo } = React;
const Item = memo(function Item({ onClick, id }) {
const rendered = useRef(0);
rendered.current++;
return (
<button _id={id} onClick={onClick}>
{id} : rendered {rendered.current} times
</button>
);
});
const App = () => {
const [message, setMessage] = useState('');
const onClick = (e) =>
setMessage(
'last clicked' + e.target.getAttribute('_id')
);
const memOnClick = useCallback(onClick, []);
return (
<div>
<h3>{message}</h3>
{[1, 2].map((id) => (
<Item key={id} id={id} onClick={onClick} />
))}
{[1, 2].map((id) => (
<Item key={id} id={id} onClick={memOnClick} />
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Another example is when you want to call a function in an effect that also needs to be called outside of the effect so you can't put the function inside the effect. You only want to run the effect when a certain value changes so you can do something like this.
//fetchById is (re) created when ID changes
const fetchById = useCallback(
() => console.log('id is', ID),
[ID]
);
//effect is run when fetchById changes so basically
// when ID changes
useEffect(() => fetchById(), [fetchById]);
Your second question:
The setValues({ ...prev, [name]: value }) will give you an error because you never defined pref but if you meant: setValues({ ...values, [name]: value }) and wrap the handler in a useCallback then now your callback has a dependency on values and will be needlessly be re created whenever values change.
If you don't provide the dependency then the linter will warn you and you end up with a stale closure. Here is an example of the stale closure as counter.count will never go up because you never re create onClick after the first render thus the counter closure will always be {count:1}.
const { useState, useCallback, useRef } = React;
const App = () => {
const [counts, setCounts] = useState({ count: 1 });
const rendered = useRef(0);
rendered.current++;
const onClick = useCallback(
//this function is never re created so counts.count is always 1
// every time it'll do setCount(1+1) so after the first
// click this "stops working"
() => setCounts({ count: counts.count + 1 }),
[] //linter warns of missing dependency count
);
return (
<button onClick={onClick}>
count: {counts.count} rendered:{rendered.current}
</button>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Categories

Resources