Selecting and deseleting all checkboxes using hooks - javascript

I have an array state for some checkboxes where I am catching the labels for those that are true (checked). Must ignore the false.
I am able to generate a list of checked checkboxes thanks to some of you in another thread. But I'm hitting another wall with the select all toggle.
const handleSwitch = (e) => {
if(e.target.checked) {
setActive(true);
const updatedCheckedState = checkedState.map(element => element.checked = true);
setCheckedState([...updatedCheckedState]);
} else {
setActive(false)
const updatedCheckedState = checkedState.map(element => element.checked = false);
setCheckedState([...updatedCheckedState]);
}
}
This function above in particular. Likewise, if I manually check all of the checkboxes inside, it needs to know that all are selescted and make the active state = true. If I can get help with at least the first part, I'm confident I can solve the other part myself.
Here's a sandbox if you want to mess around with it. Thanks

Your sandbox is quite broken. The way you are tracking checked state is internally inconsistent.)
The main culprits (in Filter.js) are:
on line 119, you treat checkedState like a dictionary, but in handleSwitch and handleOnChange you treat it like an array (but the logic inside is still non-functional for the array approach as far as I can tell.
if you want it to be an array, let it be a string-valued "checkedLabels" array, and set checked on your checkbox component to checkedLabels.includes(item.label)
if you want it to be a dictionary:
handleOnChange needs to simply toggle the currently clicked element, like so [e.target.name]: !checkedState[e.target.name]
handleSwitch needs to add an entry for every element in data, set to true or false as appropriate.
Example (codesandbox):
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedState(
Object.fromEntries(data.map((item) => [item.label.toLowerCase(), true]))
);
} else {
setActive(false);
setCheckedState({});
}
};
const handleOnChange = (e) => {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedState[item.label.toLowerCase()] ?? false}
onChange={handleOnChange}
/>
EDIT from OP
I tweaked the hnadleOnChange function to
const handleOnChange = (e) => {
if (e.target.checked) {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
} else {
const updatedCheckedState = {...checkedState};
delete updatedCheckedState[e.target.name];
setCheckedState(updatedCheckedState);
}
};
Before, it allowed for false values to be added when you unchecked a previously checked checkbox. This removes it
Edit: To do this with an array, you'll want to add to the array when checking, and remove from it when un-checking. Then do an includes to see if an individual checkbox should be checked.
Also, you can do a simple setActive(newCheckedItemLabels.length === data.length); in the handleOnChange to achieve your other requirement.
This codesandbox does everything you need with arrays instead of objects.
Notably:
const [checkedItemLabels, setCheckedItemLabels] = useState([]);
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedItemLabels(data.map((item) => item.label.toLowerCase()));
} else {
setActive(false);
setCheckedItemLabels([]);
}
};
const handleOnChange = (e) => {
const newCheckedItemLabels = checkedItemLabels.includes(e.target.name)
? checkedItemLabels.filter((label) => label !== e.target.name)
: [...checkedItemLabels, e.target.name];
setCheckedItemLabels(newCheckedItemLabels);
setActive(newCheckedItemLabels.length === data.length);
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedItemLabels.includes(
item.label.toLowerCase()
)}
onChange={handleOnChange}
/>

Adding to #JohnPaulR answer. You can add useEffect hoot to achieve additional requirements you have.
if I manually check all of the checkboxes inside, it needs to know that all are selected and make the active state = true.
useEffect(() => {
const checkedTotal = Object.keys(checkedState).reduce((count, key) => {
if (checkedState[key]) {
return count + 1;
}
}, 0);
setActive(data.length === checkedTotal);
}, [checkedState]);
A full working example https://codesandbox.io/s/still-water-btyoly

Related

React Native, State Changes, but JSX Conditional Rendering Does not Updated UI

Hello and thank you for your time in advance!
I am struggling with a small issue that I have not encountered before, with React not rendering an UI element based on a check function. Basically, what I am trying to make, is a multiple selection filter menu, where when an option is clicked, the dot next to it changes to red.
For this purpose I append the value of each option to an array, and using array.sort, verify when it is to be added (value is pushed to FilterList) and removed (value is popped from FilterList)
The checkfilter function operates normally, and when logging the state, indeed it works as intended with the array being updated.
However, the JSX code running the checkfilter to render the extra red dot inside the bigger circle, unfortunately does not.
Additionally, when the screen is refreshed, the UI is updated normally, with every option clicked, now showing the aforementioned red dot.
Why is this happening? I have tried several hooks, JSX approaches, using imported components and more that I can't even remember, yet the UI will not update oddly.
Below you can find a snippet of the code. Please bear in mind this is a render function for a flatlist component
const checkFilter = useCallback((element) => {
return filterList?.some((el: any) => (el == element))
}, [filterList])
const removeFilter = useCallback((cat) => {
let temparr = filterList
var index = temparr?.indexOf(cat);
if (index > -1) {
temparr?.splice(index, 1);
}
setFilterList(temparr)
}, [filterList])
const addFilter = useCallback((cat) => {
let temparr = filterList;
temparr.push(cat);
setFilterList(temparr)
}, [filterList])
const renderFilter = useCallback((item) => {
return (
<Pressable
onPress={() => {
checkFilter(item?.item) ? removeFilter(item?.item) : addFilter(item?.item);
console.log(filterList)
}}
style={styles.modalOptionWrapper}
>
<Text style={styles.modalOptionTitle(checkFilter)}>{item?.item}</Text>
<View style={styles.modalOptionRowRight}>
<View style={styles.radioBtn}>
{checkFilter(item?.item) ?
<View style={styles.radioBtnBullet} />
:
null
}
</View>
</View>
</Pressable>
)
}, [filterList])
This may not be correct answer but try this. When I say simple basic codes, like this.
const ListItems = () => {
const [filterList, setFilterList] = React.useState([]); // must be array
const checkFilter = filterList?.some((el) => el === element);
const removeFilter = useCallback(
(cat) => {
// not updating new state, just useing previous state
setFilterList((prev) => [...prev].filter((el) => el !== cat));
// used spread iterator
},
[] // no need dependency
);
const addFilter = useCallback((cat) => {
// used spread indicator
setFilterList((prev) => [...prev, cat]);
}, []);
const renderFilter = useCallback((item) => {
// just checking our codes is right with basic elements
// it may be 'undefined'
return <Text>{JSON.stringify(item)}</Text>;
}, []);
return filterList.map(renderFilter);
};

Why can't I append a child using useState in React?

I am using React and found something which was not quite working, but it made sense to me totally:
const [Res, setRes] = useState(<div></div>);
const test = (e) => {
if (e.keyCode === 13) {
setRes( prevState => prevState.append(<p>{e.target.value)}</p>))
}
}
return(
{Res}
)
If this us wrong, please tell me the correct way to solve similar problems please.
Keeping JSX in state is an antipattern.
Instead of keeping the JSX in an array in state, keep the data in the array without JSX, then build the JSX when you are ready to render:
const YourComponent = () => {
const [res, setRes] = useState([]);
const test = (e) => {
const {value} = e.target;
if (e.code === "Enter") {
setRes(prev => [...prev, value]);
}
};
return (
<div>
{res.map(e => <p>{e}</p>)}
</div>
);
};
<p> is using index as key, which is a problem. How to fix it is application-specific depending on where your data is coming from and whether it can be removed from the array, whether it's unique, etc, but ideally generate an id.
Also, e.target.value shouldn't be accessed in a state setter callback. It's async so the event object might have gone stale by the time it's read. Pull out the primitive value into the handler closure.
I suggest picking better names: test and res are pretty meaningless.
Finally, instead of e.keyCode === 13, use e.code === "Enter".

React useEffect and useState interaction

I am using React and Material UI to create a table (XGrid) with some buttons. When you click the row, it should set the row id using useState. When you click the delete button, it should delete the row. It seems that the delete click handler is not using the value from use state. This is either some kind of closure thing or some kind of React thing.
const MyTableThing: React.FC = (props) =>
{
const { data } = props;
const [filename, setFilename] = React.useState<string>("")
const [columns, setColumns] = React.useState<GridColDef[]>([])
const handleDelete = () =>
{
someFunctionThatDeletes(filename); // filename is always ""
setFilename(""); // Does not do anything.. !
}
React.useEffect(() =>
{
if (data)
{
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) =>
{
if (column.field === "delete")
{
column.renderCell = (cellParams: GridCellParams) =>
{
return <Button onClick={handleDelete}>Delete</Button>
}
}
})
setColumns(columns)
}
}, [data?.files])
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) =>
{
console.log(`set selected row to ${param.data.id}`) // This works every time
setFilename(param.data.id)
}
}
The reason for this behavior is that React does not process setState action synchronously. It is stacked up with other state changes and then executed. React does this to improve performance of the application. Read following link for more details on this.
https://linguinecode.com/post/why-react-setstate-usestate-does-not-update-immediately
you can disable your deleteRow button till the filename variable is updated. you can use useEffect or setState with callback function.
useEffect(() => {
//Enable your delete row button, fired when filename is updated
}, filename)
OR
this.setFilename(newFilename, () => {
// ... enable delete button
});
Let me know if this helps! Please mark it as answer if it helps.
The main problem I see here is that you are rendering JSX in a useEffect hook, and then saving the output JSX into columns state. I assume you are then returning that state JSX from this functional component. That is a very bizarre way of doing things, and I would not recommend that.
However, this explains the problem. The JSX being saved in state has a stale version of the handleDelete function, so that when handleDelete is called, it does not have the current value of filename.
Instead of using the useEffect hook and columns state, simply do that work in your return statement. Or assign the work to a variable and then render the variable. Or better yet, use a useMemo hook.
Notice that we add handleDelete to the useMemo dependencies. That way, it will re-render every time handleDelete changes. Which currently changes every render. So lets fix that by adding useCallback to handleDelete.
const MyTableThing: React.FC = (props) => {
const { data } = props;
const [filename, setFilename] = React.useState<string>('');
const handleDelete = React.useCallback(() => {
someFunctionThatDeletes(filename); // filename is always ""
setFilename(''); // Does not do anything.. !
}, [filename]);
const columns = React.useMemo(() => {
if (!data) {
return null;
}
let columns: GridColumns = data.columns;
columns.forEach((column: GridColDef) => {
if (column.field === 'delete') {
column.renderCell = (cellParams: GridCellParams) => {
return <Button onClick={handleDelete}>Delete</Button>;
};
}
});
return columns;
}, [data?.files, handleDelete]);
// Called when a row is clicked
const handleRowSelected = (param: GridRowSelectedParams) => {
console.log(`set selected row to ${param.data.id}`); // This works every time
setFilename(param.data.id);
};
return columns;
};

Weird Reselect selector behavior

For some reason, my selector function only gets called when one of the arguments change but not the other.
Here is my selector that gets transactions from state and applies 2 filters to them
export const getFilteredTransactionsSelector = createSelector(
(state) => state.transactions.transactions,
(items) =>
memoize((filterValueFirst, filterValueSecond) =>
items
.filter((item) => {
if (filterValueFirst === "Show All") {
return true;
}
return item["Status"] === filterValueFirst;
})
.filter((item) => {
if (filterValueSecond === "Show All") {
return true;
}
return item["Type"] === filterValueSecond;
})
)
);
In my component's mapStateToProps I pass current state to the selector
const mapStateToProps = (state) => ({
transactions: getTransactions(state),
getFilteredTransactions: getFilteredTransactionsSelector(state),
...
});
And then I call it whenever one of the filter values changes
useEffect(() => {
setFilteredTransactions(
getFilteredTransactions(statusFilterValue, typeFilterValue)
);
}, [transactions, statusFilterValue, typeFilterValue]);
The problem is that I get filtered data only when I change the first filter's value (statusFilterValue). If I change the 2nd one, nothing happens despite the fact that the useEffect hook gets called as it should be.
If I put console.log inside memoize function, it will only show the result if I change the first filter but not the second. Any help would be appreaciated
Going from the code in your question you don't need memoize at all. If you did need it then better use the one that is already in reselect and prevent adding unnecessary dependencies.
You also don't need an effect since what selectFilteredTransactions returns will not change as long as items, filterValueFirst or filterValueSecond won't change and if setFilteredTransactions comes from useState then passing the same value to it between renders won't cause any re render.
You can create the selector like so:
export const selectFilteredTransactions = createSelector(
(state) => state.transactions.transactions,
(a, filterValueFirst) => filterValueFirst,
(a, b, filterValueSecond) => filterValueSecond,
(items, filterValueFirst, filterValueSecond) =>
console.log('running selector, something changed') ||
items
.filter((item) => {
if (filterValueFirst === 'Show All') {
return true;
}
return item['Status'] === filterValueFirst;
})
.filter((item) => {
if (filterValueSecond === 'Show All') {
return true;
}
return item['Type'] === filterValueSecond;
})
);
And call it like this:
//don't even need the effect here since filteredTransactions
// only changes when items, filterValueFirst or filterValueSecond
// changes
setFilteredTransactions(//assuming this comes from useState
useSelector((state) =>
selectFilteredTransactions(
state,
statusFilterValue,
typeFilterValue
)
)
);

How do I handle onChange of objects inside an array (React State)?

community,
I'm trying to build a table where its rows are rendered based on an array of objects:
this.state = {
tableContent: [
{
id: this.state.tableContent.length,
firstname:"",
lastname:""
},
...
]
Each row displays one object. (2 columns in this case, n rows)
Each cell is also an input field, so users can manipulate the table:
<tbody>
{this.state.tableContent.map((row) => {
return(
<tr key={row.id}>
<td><input value={row.firstname} onChange={this.handleFirstNameChange}> </input></td>
...
</tr>
)
})}
</tbody>
I want each cell / input field to display the change when the user changes the input value and as such the state. Because I'm prepopulating the input field with value={row.firstname} I need to define an onChange handler function that changes the state/value of the target object's firstnameproperty.
So how does my onChange handler function look like?
I tried using spreads, but to no avail so far...
Some thoughts:
Using the standard procedure doesn't work because I have a nested state (Array of objects):
handleChange = (event) => { this.setState({ firstname: event.target.value }) }
Trying to use the spread operator results in some weird mess as well:
(this code here is somewhat wrong: maybe you can fix it?)
handleFirstNameChange = (event) => {
const {tableContent} = {...this.state};
const currentState = tableContent;
const { firstname, value } = event.target;
currentState[0] = value;
this.setState({ tableContent: currentState});
}
I appreciate any help!
edit:
The code below seems to nearly work. (thanks #Nikhil ) However now, whenever the user types into the input field, every letter they type will replace the existing letter / value of 'row.firstname'. Also, state doesn't refresh automatically so only the last-typed letter would show up / persist. What I need the input field to have is a functionality just like any casual input field.
event.persist(); seems to be needed to keep the event value.
handleFirstNameChange = (id, event) => {
event.persist();
this.setState(prevState => {
tableContent : prevState.tableContent.forEach((row) => {
if(row.id === id) { row.firstname = event.target.value}
})
})
}
input looks like this:
onChange={(event) => this.handleWNRChange(row.id,event)}
I think something like this would work.
const handleFirstNameChange = (id, event) => {
event.preventDefault();
const {tableContext} = this.state;
const myRowIndex = tableContext.findIndex((row) => row.id === id);
tableContext[myRowIndex].firstname = event.target.value;
this.setState({ tableContext });
}
This should be all you need. Just assign this method to the onChange of the input element. Like so:
onChange={(event) => this.handleFirstNameChange(row.id, event)}
May be this will help
{this.state.tableContent.map((row, index) => {
return(
<tr key={row.firstname}>
<td><input value={row.firstname}
onChange={(event)=>this.handleFirstNameChange(event, index)> </input></td>
</tr>
)
})
}
handleFirstNameChange(event, index){
this.setState((prevState => {
tableContent : prevState.tableContent.forEach((row,i)=>{
if(index === i) row.firstname = event.target.value
})
})
}

Categories

Resources