Error when trying to change MUI (Material UI) checkbox value via state and onChange in React/NextJS - reactjs

The error is: TypeError: filters.brand.find is not a function
and it says that it happened here:
checked={
173 | filters.brand.find((item) => item === brand)
| ^
174 | ? true
175 | : false
176 | }
Below is what I'm trying to achieve:
I'm building an e-commerce site using NextJS and amongst other features, users can choose to view products by category or subcategory. On either of these pages, users can choose to refine the results even further using filters. One of these filters should allow the user to display only the items of a certain brand/s.
These brands are presented to the user as checkboxes. So a checked checkbox should mean "include this brand" otherwise do not:
<FormGroup>
{brands.map((brand) => (
<FormControlLabel
control={
<Checkbox
size="small"
name={brand}
checked={
filters.brand.find((item) => item === brand)
? true
: false
}
onChange={() => handleBrandChange(brand)}
/>
}
label={brand}
key={brand}
/>
))}
</FormGroup>
btw: I tried using "includes" instead of "find" above but still ran into the same problem.
The initial value for filter.brand comes from the URL's query string.
const { query, ...router } = useRouter();
const [filters, setFilters] = useState({
brand: query.brand ? (typeof query.brand === "string" ? [query.brand] : query.brand) : [],
});
I check if the query string has "brand", which could either come back as a string or an array. If it is there, I check its type. If it's a string, I set filter.brand's value to an array with that string inside otherwise I take it as is. If the query string does not have a brand, I assign filter.brand an empty array.
btw, I am leaving out most of the code that is of no relevance to this problem.
Below is how I tried to handle the change (when the user clicks on a brand checkbox):
const handleBrandChange = (brand) => {
const brandArray = filters.brand;
if (brandArray.find((item) => item === brand)) {
setFilters({ ...filters, brand: brandArray.filter((item) => item !== brand) });
} else setFilters({ ...filters, brand: brandArray.push(brand) });
};
and below is what should happen every time the state changes:
useEffect(() => {
const urlObject = {
query: {
category: query.category,
sortBy: filters.sortBy,
maxPrice: filters.maxPrice,
},
};
if (filters.brand.length > 0) urlObject.query.brand = filters.brand;
if (query.subcategory) {
urlObject.pathname = "/[category]/[subcategory]";
urlObject.query.subcategory = query.subcategory;
} else {
urlObject.pathname = "/[category]";
}
router.push(urlObject);
}, [filters, query.subcategory, query.category]);
Sorry for the messy question. I am not used to asking questions on here (this is only my second time), I have not been coding for that long and English is not my first language. Any help would be greatly appreciated.

Related

Select all checkboxes in a React functional component failing, I suspect due to render happening too fast

Edit:
The solution, as sschwei1 pointed out in the comments, is to do the following in the funciton where I add selected values:
const addToSelected = (user: any) => {
let tempSelectedUser: any[] = selectedUsers;
if (tempSelectedUser.indexOf(user) === -1)
setSelectedUsers([...tempSelectedUser, user]);
};
I've written a component that contains a range of checkboxes with the option to select all. Each checkbox is related to a JSON data object that I have stored in an array in the state named "data".
I am using the Ant Design package for my components BTW: https://ant.design/components/checkbox/
I've written what to me looks like a fairly elegant solution to select and deselect individual or all items. This approach will ensure that whatever is in my selectedUsers variable in the state will determine whether each checkbox will display if it is checked or not, including the "select all" one.
My code seems to work well, but my checkboxes are not displaying as checked when they are in the list of selected items. When I output the values to the console, however, the script that determines whether an element should be selected seems to return true without displaying that the element is selected. (note the console.log in my code which returns false the first time, then always true)
I suspect there is a delay between when the item is added to the list of selected elements and when the component renders, causing the script to return false at render time. I've considered reloading the component a few milliseconds later, but that seems like a hackey solution.
Here are some snippets that show what I am trying:
...
const [data, setData] = useState<any[]>([]);
const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
...
const addToSelected = (user: any) => {
let tempSelectedUser: any[] = selectedUsers;
tempSelectedUser.indexOf(user) === -1 && tempSelectedUser.push(user);
setSelectedUsers(tempSelectedUser);
};
const removeFromSelected = (user: any) => {
let setValue = selectedUsers;
let tempSelectedUser = setValue.filter((el: any) => {
return el.id !== user.id;
});
setSelectedUsers(tempSelectedUser);
};
const userSelected = (userToFind: any) => {
let found = false;
selectedUsers.map((user: any) => {
if (userToFind.id === user.id) found = true;
});
return found;
};
...
<Checkbox
className={"Select All"}
onChange={(e) => {
e.target.checked
? setSelectedUsers(data)
: setSelectedUsers([]);
}
checked={selectedUsers === data}
/>
...
{users.map((user: any) => (
...
<Checkbox
checked={userSelected(user)}
onChange={(e) => {
e.target.checked
? addToSelected(user)
: removeFromSelected(user);
}}
/>
...
)}
...
Here is a quick screenshot of the interface as well as my console
Edit: I just double-checked and the select all function is working at least. I can also see when I select and deselect items that after I do something else that causes the component to refresh, that the values that I expect to see displays as checked. I'm going to implement a refresh for the time being, but I'm really hoping for a more elegant solution.
Edit: So the forced refresh is also not giving me the expected result. I've also noticed that the deselect works perfectly, but I can't seem to select a checkbox besides through the select all option.
Edit: I've changed the check to see if the user is in the selectedUsers state variable to a function that rather compares the user's ID's bases on #classydraught's suggestion (thanks BTW). It does not affect the functionaity unfortunatly.
Here is also an example of the data that can be found in the "data" object (note this this is largely dummy data for now):
[
{
createdAt: null
description: null
email: "sysadmin#ilabqa.com"
id: 1
imageLink: "http://localhost:3000/api/v1/user/1/image"
lastActivity: null
name: "System Admin"
role: null
status: "ACTIVE"
username: "sysadmin"
},
{
createdAt: null
description: null
email: "testadmin#ilabqa.com"
id: 2
imageLink: "http://localhost:3000/api/v1/user/2/image"
lastActivity: null
name: "Test Admin"
role: null
status: "DEACTIVE"
}
]

Dynamically populate a Material UI Select Component, depending on other states

I'm currently stuck with the following scenario.
I have two select components from Material-UI.
Select component one contains car brands, component two contains car models.
Desired behaviour
The car brands get fetched from the server and the list for Select "brands" gets populated.
Select "models" is still disabled, until a brand is selected.
Brand gets selected, client is fetching all models from this brand from server
Options for the Select 'models' get populated and the component becomes enabled
I store the fetched brand in a redux state.
I store the fetched model in a redux state.
I store the selected brand and model in a redux state.
{
brand:{
brands:{
brands:[
{
_id: 1,
name: "vw"
},
{
_id: 2,
name: "bmw"
}
]
}
},
data:{
form: {
brandId: '',
modelId: '',
errors: {}
},
models:{}
}
}
redux-states
Current behaviour
Car makers get fetched from server and the options for Select component "makers" gets populated as expected.
Select "models" is still disabled, as it should be.
I click on the "brands" select and see the available brands, as expected.
I select a brand and the following happens
"brands" select doesn't show any value and if i click on it again, the list is empty
the following warning appears
SelectInput.js:342 Material-UI: You have provided an out-of-range value
Project setup
let onChange = event => {
dispatch({
type: 'UPDATE_USER_INVITE_FORM',
field: event.target.id,
payload: event.target.value
});
};
...
return(
...
<FormControl
variant="outlined"
className={classes.formControl}
error={form.errors.brandId? true : false}
disabled={brandRenderList.length < 0}>
<InputLabel id="brand-label">Brand</InputLabel>
<Select
labelId="brand-label"
id="brandId"
label="Brand"
value={form.brandIdId}
onChange={(e) => { e.target.id = 'companyId'; onChange(e) }}
>
{brandRenderList}
</Select>
<FormHelperText>{form.errors.brandId}</FormHelperText>
</FormControl>
<FormControl
variant="outlined"
className={classes.formControl}
error={form.errors.modelId? true : false}
disabled={modelRenderList.length < 0}>
<InputLabel id="model-label">Model</InputLabel>
<Select
labelId="model-label"
id="modelId"
label="Model"
value={form.modelId}
onChange={(e) => { e.target.id = 'modelId'; onChange(e) }}
>
{modelRenderList}
</Select>
<FormHelperText>{form.errors.modelId}</FormHelperText>
</FormControl>
...
)
MyComponent.js
const selectedBrandId = useSelector(state => state.data.form.brandId);
// fetch brands
const brands = useSelector(state => state.brand.brands);
useEffect(() => {
if (Object.keys(brands).length === 0) {
dispatch(getBrands())
} else if (brandRenderList.length <= 0) {
brands.brands.forEach(brand => {
brandRenderList.push(<MenuItem value={brand._id} key={brand._id}>{brand.name}</MenuItem>)
})
}
}, [brands])
// fetch models
const models = useSelector(state => state.model.models);
useEffect(() => {
if(!selectedBrandId ){return}
else if (Object.keys(models).length === 0) {
dispatch(getModels({brandId: selectedBrandId }))
} else if (modelRenderList.length <= 0) {
models.models.forEach(model => {
modelRenderList.push(<MenuItem value={model._id} key={model._id}>{model.name}</MenuItem>)
})
}
}, [brands])
ParentComponent.js
If I put a static brandId, everything works just as desired. Obviously I want a dynamic one, which reacts to the user input in the brand select component.
// this works
const selectedBrandId = '6018f90b94a59215703adba0';
// this doesn't
const selectedBrandId = useSelector(state => state.data.form.brandId);
I appreciate any feedback :)
Hi mate i feel you i have been stuck for the same things few week ago at work !
I'm asking myself who had texted this nonsense warning alert: "You have provided an out-of-range value " you get this error just because the selectbox needs a defaultValue property. Insane bullsh** i agree and not really helpful for debug !
Hope you can fix it and keep coding have a great day !

Filter products based on searchString and product size

I'm rather new to React and still learning. I'm trying to filter my overview of products based on a search string and/or with a filter based on the size state.
Right now, the searching works and I'm able to search within this product view
My search works this way: I take the value from the input and insert it into the {this.state.plants.filter you can see in the bottom of the sample code below.
My question is: How do I filter the products by this.state.size AND this.state.searchString at the same time?
OR: Leave the this.state.searchString empty and only applying this.state.size?
I hope my question makes sense.
this.state = {
plants: '',
title: '',
size: '',
searchString: '',
filterSize: ''
};
handleSearch = (event) => {
this.setState({
searchString: event.target.value
})
}
handleSizeFilter = (event) => {
this.setState({
filterSize: event.target.value
})
}
// Search input
<input type="text" className="search" placeholder="Søg" onChange={this.handleSearch} value={this.state.searchString} />
// Size Filter Button
<button onClick={handleSizeFilter} value="22">Size 22</button>
{this.state.plants.filter(plant => plant.title.includes(this.state.searchString)).map(plant => (
{plant.title} - {plant.size}
))}
What I would do here is something like:
let filteredPlants = this.state.plants;
const { searchString, filterSize } = this.state;
if (searchString) {
filteredPlants = filteredPlants.filter(plant => plant.title.includes(searchString);
}
if (typeof filterSize === 'number') {
filteredPlants = filteredPlants.filter(plant => plant.size === filterSize);
}
NOTE: Since this is a potentially expensive operation depending on how big your arrays are, best to create a method inside your class component and wrap it in a utility function like memoizeOne.
If you put this inside your render method, the component will iterate through the array every time you re-render - so in your case every single time you type or delete something in your input fields

State update problem with in more than 20 fields

I am working on React JS application where multiple type of the questions and multiple type of fields are in the form. I will post the code only for the one type of question.
To update the state:-
const handleChange = ( e: string, qInd: number ): void => {
const previousState: any = [...state.freeTextAnswers]; // copying the old datas array
previousState[qInd] = e;
setState( prevState => {
return {
...prevState,
freeTextAnswers: previousState};
})
}
Render Part is:
<FormControl
className="border border-info "
as="textarea"
name={`questions.question-${props.index}-${question.question_type_id}[${qInd}].text`}
onChange={(
ev: React.ChangeEvent<HTMLTextAreaElement>,
): void => handleChange(ev.target.value, qInd)}
minLength={5}
autoComplete="off"
maxLength={question.char_limit_answer}
disabled={currentProgress === 4 ? true : false }
readOnly={ currentProgress === 4 ? true : false }
id={`questions.question-${index}-${question.question_type_id}[${qInd}].text`}
/>
<FormError touched={props.formik.touched} subpath={`question-${index}-${question.question_type_id}[${qInd}]`} errors = {formik.errors} path={`question-${props.index}-${question.question_type_id}-${qInd}`} />
I am using Formik and handle the event manually just for speedup
useEffect(() => {
if( question ){
const freeTextAnswers = state.freeTextAnswers;
const getFreeAnswers = me.map((a, index) => ({
answer_id: a.answer_id,
option_answer_id: a.option_answer_id,
text: freeTextAnswers[index] ? freeTextAnswers[index] : '',
question_id: question.question_id,
is_predefined: false
}));
props.formik.setFieldValue(`questions.question-${props.index}-${question.question_type_id}`, getFreeAnswers)
}
}, [state.freeTextAnswers])
This above snippet of code is making it slower.
How can I speed it up so every time state change it doesn't enter in the useEffect and make it slower.
By "slower" do you mean to say that it is running on each render? If so, you can use a debounce on the input, so that it will only fire once the user has "finished" typing out the input.

React Redux - select all checkbox

I have been searching on Google all day to try and find a way to solve my issue.
I've created a "product selection page" and I'm trying to add a "select all" checkbox that will select any number of products that are displayed (this will vary depending on the customer).
It's coming along and I've got all the checkboxes working but I can't get "select all" to work. Admittedly I'm using some in-house libraries and I think that's what's giving me trouble as I'm unable to find examples that look like what I've done so far.
OK, so the code to create my checkboxGroup is here:
let productSelectionList = (
<FormGroup className="productInfo">
<Field
component={CheckboxGroup}
name="checkboxField"
vertical={true}
choices={this.createProductList()}
onChange={this.handleCheckboxClick}
helpText="Select all that apply."
label="Which accounts should use this new mailing address?"
/>
</FormGroup>
);
As you can see, my choices will be created in the createProductList method. That looks like this:
createProductList() {
const { products } = this.props;
const selectAllCheckbox = <b>Select All Accounts</b>;
let productList = [];
productList.push({ label: selectAllCheckbox, value: "selectAll" });
if (products && products.length > 0) {
products.forEach((product, idx) => {
productList.push({
label: product.productDescription,
value: product.displayArrangementId
});
});
}
return productList;
}
Also note that here I've also created the "Select All Accounts" entry and then pushed it onto the list with a value of "selectAll". The actual products are then pushed on, each having a label and a value (although only the label is displayed. The end result looks like this:
Select Products checkboxes
I've managed to isolate the "select all" checkbox with this function:
handleCheckboxClick(event) {
// var items = this.state.items.slice();
if (event.selectAll) {
this.setState({
'isSelectAllClicked': true
});
} else {
this.setState({
'isSelectAllClicked': false
});
}
}
I also created this componentDidUpdate function:
componentDidUpdate(prevProps, prevState) {
if (this.state.isSelectAllClicked !== prevState.isSelectAllClicked && this.state.isSelectAllClicked){
console.log("if this ", this.state.isSelectAllClicked);
console.log("if this ", this.props);
} else if (this.state.isSelectAllClicked !== prevState.isSelectAllClicked && !this.state.isSelectAllClicked){
console.log("else this ", this.state.isSelectAllClicked);
console.log("else this ", this.props);
}
}
So in the console, I'm able to see that when the "select all" checkbox is clicked, I do get a "True" flag, and unclicking it I get a "False". But now how can I select the remaining boxes (I will admit that I am EXTREMELY new to React/Redux and that I don't have any previous checkboxes experience).
In Chrome, I'm able to see my this.props as shown here..
this.props
You can see that this.props.productList.values.checkboxField shows the values of true for the "select all" checkbox as well as for four of the products. But that's because I manually checked off those four products for this test member that has 14 products. How can I get "check all" to select all 14 products?
Did I go about this the wrong way? (please tell me that this is still doable) :(
My guess is your single product checkboxes are bound to some data you have in state, whether local or redux. The checkbox input type has a checked prop which accepts a boolean value which will determine if the checkbox is checked or not.
The idea would be to set all items checked prop (whatever you are actually using for that value) to true upon clicking the select all checkbox. Here is example code you can try and run.
import React, { Component } from 'react';
import './App.css';
class App extends Component {
state = {
items: [
{
label: "first",
checked: false,
},
{
label: "last",
checked: false,
}
],
selectAll: false,
}
renderCheckbooks = (item) => {
return (
<div key={item.label}>
<span>{item.label}</span>
<input type="checkbox" checked={item.checked} />
</div>
);
}
selectAll = (e) => {
if (this.state.selectAll) {
this.setState({ selectAll: false }, () => {
let items = [...this.state.items];
items = items.map(item => {
return {
...item,
checked: false,
}
})
this.setState({ items })
});
} else {
this.setState({ selectAll: true }, () => {
let items = [...this.state.items];
items = items.map(item => {
return {
...item,
checked: true,
}
})
this.setState({ items })
});
}
}
render() {
return (
<div className="App">
{this.state.items.map(this.renderCheckbooks)}
<span>Select all</span>
<input onClick={this.selectAll} type="checkbox" checked={this.state.selectAll} />
</div>
);
}
}
export default App;
I have items in state. Each item has a checked prop which I pass to the checkbox getting rendered for that item. If the prop is true, the checkbox will be checked otherwise it wont be. When I click on select all, I map thru my items to make each one checked so that the checkbox gets checked.
Here is a link to a codesandbox where you can see this in action and mess with the code.
There is a package grouped-checkboxes which can solve this problem.
In your case you could map over your products like this:
import React from 'react';
import {
CheckboxGroup,
AllCheckerCheckbox,
Checkbox
} from "#createnl/grouped-checkboxes";
const App = (props) => {
const { products } = props
return (
<CheckboxGroup onChange={console.log}>
<label>
<AllCheckerCheckbox />
Select all accounts
</label>
{products.map(product => (
<label>
<Checkbox id={product.value} />
{product.label}
</label>
))}
</CheckboxGroup>
)
}
More examples see https://codesandbox.io/s/grouped-checkboxes-v5sww

Resources