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

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 !

Related

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

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.

useEffect removes state and then resets? React

I have a component made with React Select. The options passed into the options prop on the Select depends upon state that users entered previously. Every time the component renders there are checks to see if selectOptions already includes items from the state array
<Select
styles={err === '' ? inputStyles : inputStylesErr}
className="basic-single"
classNamePrefix="select"
isClearable={true}
isSearchable={true}
isMulti={true}
placeholder={`Select or search health zones in ${province}, ${state.address.country}`}
options={selectOptions}
defaultValue={selectOptions.some((option) => option.value === state.healthZonesServed[0]) ? (
state.healthZonesServed.map((zone) => {
return { ['label']: zone, ['value']: zone }
})
) : ''}
onChange={(values) => handleAddHealthZones(values.map((value) => value.value))}
/>
const handleAddHealthZones = (value) => {
setState({
...state,
healthZonesServed: value
})
}
If a user populated their healthZonesServed array and then goes back and changes their province (the piece of state which controls the selectOptions) and then goes back to this component I need the healthZonesServed array to be reset to []
I do this in a useEffect. I can see in my console.log the healthZonesServed resets to an empty array on page load then somehow re-populates its previous values from somewhere. Would anyone have any insight as to why this is happening and a possible solution?
useEffect(() => {
if (selectOptions.some((option) => option.value === state.healthZonesServed[0])) {
return
} else {
setState({
...state,
healthZonesServed: []
})
console.log('HIT')
}
}, [])
The most probable reason, why this is not working is because you are using setState in a functional component. Try using the useState hook for the purpose of managing the state, in your case, setting the heathZoneServed array to empty array.
const [healthZoneServed,sethealthZoneServed] = useState([]);
sethealthZoneServed(value);
useEffect(() => {
if (selectOptions.some((option) => option.value === state.healthZonesServed[0])) {
return;
} else {
sethealthZonesServed([]);
console.log('HIT');
}
}, [healthZonesServed]);
Hope this was helpful.

Why is my data that is coming from apollo server not showing up when I refresh the page?

I am building a simple application using React, Apollo and React Router. This application allows you to create recipes, as well as edit and delete them (your standard CRUD website).
I thought about how I would present my problem, and I figured the best way was visually.
Here is the home page (localhost:3000):
When you click on the title of a recipe, this is what you see (localhost:3000/recipe/15):
If you click the 'create recipe' button on the home page, this is what you see (localhost:3000/create-recipe):
If you click on the delete button on a recipe on the home page, this is what you see (localhost:3000):
If you click on the edit button on a recipe on the home page, this is what you see (localhost:3000/recipe/15/update):
This update form is where the problem begins. As you can see, the form has been filled with the old values of the recipe. Everything is going to plan. But, when I refresh the page, this is what you see:
It's all blank. I am 67% sure this is something to do with the way React renders components or the way I am querying my apollo server. I don't fully understand the process React goes through to render a component.
Here is the code for the UpdateRecipe page (what you've probably been waiting for):
import React, { useState } from "react";
import { Button } from "#chakra-ui/react";
import {
useUpdateRecipeMutation,
useRecipeQuery,
useIngredientsQuery,
useStepsQuery,
} from "../../types/graphql";
import { useNavigate, useParams } from "react-router-dom";
import { SimpleFormControl } from "../../shared/SimpleFormControl";
import { MultiFormControl } from "../../shared/MultiFormControl";
interface UpdateRecipeProps {}
export const UpdateRecipe: React.FC<UpdateRecipeProps> = ({}) => {
let { id: recipeId } = useParams() as { id: string };
const intRecipeId = parseInt(recipeId);
const { data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const { data: ingredientsData } = useIngredientsQuery({
variables: { recipeId: intRecipeId },
});
const { data: stepsData } = useStepsQuery({
variables: { recipeId: intRecipeId },
});
const originalTitle = recipeData?.recipe.recipe?.title || "";
const originalDescription = recipeData?.recipe.recipe?.description || "";
const originalIngredients =
ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text) || [];
const originalSteps = stepsData?.steps?.steps?.map((stp) => stp.text) || [];
const [updateRecipe] = useUpdateRecipeMutation();
const navigate = useNavigate();
const [formValues, setFormValues] = useState({
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<SimpleFormControl
label="Title"
name="title"
type="text"
placeholder="Triple Chocolate Cake"
value={formValues.title}
onChange={(e) => {
setFormValues({ ...formValues, title: e.target.value });
}}
/>
<SimpleFormControl
label="Description"
name="description"
type="text"
placeholder="A delicious combination of cake and chocolate that's bound to mesmerize your tastebuds!"
value={formValues.description}
onChange={(e) => {
setFormValues({ ...formValues, description: e.target.value });
}}
/>
<MultiFormControl
label="Ingredients"
name="ingredients"
type="text"
placeholder="Eggs"
values={formValues.ingredients}
onAdd={(newValue) => {
setFormValues({
...formValues,
ingredients: [...formValues.ingredients, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
ingredients: formValues.ingredients.filter(
(__, idx) => idx !== index
),
});
}}
/>
<MultiFormControl
ordered
label="Steps"
name="steps"
type="text"
placeholder="Pour batter into cake tray"
color="orange.100"
values={formValues.steps}
onAdd={(newValue) => {
setFormValues({
...formValues,
steps: [...formValues.steps, newValue],
});
}}
onDelete={(_, index) => {
setFormValues({
...formValues,
steps: formValues.steps.filter((__, idx) => idx !== index),
});
}}
/>
<Button type="submit">Update Recipe</Button>
</form>
);
};
I'll try to explain it as best as I can.
First I get the id parameter from the url. With this id, I grab the corresponding recipe, its ingredients and its steps.
Next I put the title of the recipe, the description of the recipe, the ingredients of the recipe and the steps into four variables: originalTitle, originalDescription, originalIngredients and originalSteps, respectively.
Next I set up some state with useState(), called formValues. It looks like this:
{
title: originalTitle,
description: originalDescription,
ingredients: originalIngredients,
steps: originalSteps,
}
Finally, I return a form which contains 4 component:
The first component is a SimpleFormControl and it is for the title. Notice how I set the value prop of this component to formValues.title.
The second component is also a SimpleFormControl and it is for the description, which has a value prop set to formValues.description.
The third component is a MultiFormControl and it's for the ingredients. This component has its value props set to formValues.ingredients.
The fourth component is also aMultiFormControl and it's for the steps. This component has its value props set to formValues.steps.
Let me know if you need to see the code for these two components.
Note:
When I come to the UpdateRecipe page via the home page, it works perfectly. As soon as I refresh the UpdateRecipe page, the originalTitle, originalDescripion, originalIngredients and originalSteps are either empty strings or empty arrays. This is due to the || operator attached to each variable.
Thanks in advance for any feedback and help.
Let me know if you need anything.
The problem is that you are using one hook useRecipeQuery that will return data at some point in the future and you have a second hook useState for your form that relies on this data. This means that when React will render this component the useRecipeQuery will return no data (since it's still fetching) so the useState hook used for your form is initialized with empty data. Once useRecipeQuery is done fetching it will reevaluate this code, but that doesn't have any effect on the useState hook for your form, since it's already initialized and has internally cached its state. The reason why it's working for you in one scenario, but not in the other, is that in one scenario your useRecipeQuery immediately returns the data available from cache, whereas in the other it needs to do the actual fetch to get it.
What is the solution?
Assume you don't have the data available for your form to properly render when you first load this component. So initialize your form with some acceptable empty state.
Use useEffect to wire your hooks, so that when useRecipeQuery finishes loading its data, it'll update your form state accordingly.
const { loading, data: recipeData } = useRecipeQuery({
variables: { id: intRecipeId },
});
const [formValues, setFormValues] = useState({
title: "",
description: "",
ingredients: [],
steps: [],
});
useEffect(() => {
if (!loading && recipeData ) {
setFormValues({
title: recipeData?.recipe.recipe?.title,
description: recipeData?.recipe.recipe?.description,
ingredients: ingredientsData?.ingredients?.ingredients?.map((ing) => ing.text),
steps: stepsData?.steps?.steps?.map((stp) => stp.text),
});
}
}, [loading, recipeData ]);

set default value for select menu from mongodb

I have a dropdown list which is populated from a database. The first option is 'none' (actual record in database with objectId) which should be the default option and only needs to be changed if the user wants to, otherwise it should just use that initial value when submitting the form. However, even though it is selected and has a valid objectId, I still get a validation error saying the field is empty. The validation error only goes away if I select something else from the select menu or select something else and then select 'none' again. I am using Joi-browser for validation.
schema = {
subcategoryId: Joi.string()
.required()
.label("Subcategory"),
}
This is the select menu:
<Form onSubmit={this.handleSubmit}>
<Form.Group controlId="subcategoryId">
<Form.Label>Sub-category</Form.Label>
<Form.Control
as="select"
name="subcategoryId"
value={this.state.data.subcategoryId}
onChange={this.handleChange}
error={this.state.errors.subcategory}
>
{this.state.subcategories.map(subcategory => (
<option key={subcategory._id} value={subcategory._id}>
{subcategory.name}
</option>
))}
</Form.Control>
{this.state.errors.subcategoryId && (
<Alert variant="danger">
{this.state.errors.subcategoryId}
</Alert>
)}
</Form.Group>
And here is my state:
state = {
data: {
name: "",
description: "",
categoryId: "",
subcategoryId: "",
price: ""
},
categories: [],
subcategories: [],
errors: {}
};
const { data: subcategories } = await getSubcategories();
this.setState({ subcategories });
And this is the html output of the dropdown's first field which I want selected by default:
<option value="5d4b42d47b454712f4db7c67">None</option>
The error I get back is that the category Id cannot be empty, yet each option in the select menu has a value. I am new to react but perhaps the value is only actually assigned upon change?
You need to edit componentDidMount. After you get your subcategories, you'll need to set the state to of this.state.data.subcategoryId to one of the categories. This is because you're using a controlled component. Otherwise, it'll still be set to "", which isn't one of the valid values for the <select> component, and likely why it's failing validation.
async componentDidMount() {
// getting a copy of this.state.data so as not to mutate state directly
const data = { ...this.state.data };
const { data: subcategories } = await getSubcategories();
// filter the array to a new array of subcategories that have the name === 'none'
const arrayOfSubcategoriesWhereNameIsNone = subcategories.filter(i => i.name === 'none');
const getIdOfFirstElementOfArray = arrayOfSubcategoriesWhereNameIsNone [0]._id;
//set getIdOfFirstElementOfArray equal to the function's local copy of this.state.data.subcategoryId
data.subcategoryId = getIdOfFirstElementOfArray;
// update the state with the mutated object (the function's local copy of this.state.data)
this.setState({ subcategories, data });
}

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