Select fields not being populated by state - reactjs

I have created a page with a multiple state controlled input fields but am having difficulty ensuring that they retain the correct text values on the front end after deleting a row of elements.
Here is an image of the component:
Here is the component which displays the inputs:
<InputContainer>
<PrimaryLabel>Criteria</PrimaryLabel>
{currentRuleCriteria.map((criteria, index) => (
<TripleInputContainer>
<StyledTriplePrimarySelect name="criteria-column" id="criteria-column-select" value={currentRuleCriteria[index].column} onChange={(e) => handleCriteriaColumnChange(index, e.target.value)}>
{columns.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledTriplePrimarySelect>
<StyledTriplePrimarySelect name="criteria-operator" id="criteria-operator-select" value={currentRuleCriteria[index].operator} onChange={(e) => handleCriteriaOperatorChange(index, e.target.value)}>
{criteriaOperators.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledTriplePrimarySelect>
<InputTypeSelectorsContainer>
<StyledColumnIcon active={criteria.valueType === "column"} onClick={() => handleCriteriaValueTypeChange(index, "column")} />
<StyledTextIcon active={criteria.valueType === "text"} onClick={() => handleCriteriaValueTypeChange(index, "text")} />
</InputTypeSelectorsContainer>
{ criteria.valueType === "column" ? (
<StyledPrimarySelect name="criteria-value" id="criteria-value-select" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)}>
{columns.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledPrimarySelect>
) : (
<StyledPrimaryInput type="text" placeholder="" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)} />
)}
<RemoveItemContainer onClick={() => handleRemoveCriteria(index)}>
<StyledRemoveItemIcon active={currentRuleCriteria.length > 1} />
</RemoveItemContainer>
</TripleInputContainer>
))}
<AdditionalEntryContainer>
<AdditionalEntryButton onClick={() => handleAddCriteria()}>+ Add another</AdditionalEntryButton>
</AdditionalEntryContainer>
</InputContainer>
Here is the state which is mapped over to produce the input elements:
const [currentRuleCriteria, setCurrentRuleCriteria] = useState([{
column: "title",
operator: "contains",
value: "",
valueType: "text"
}]);
Here are the handler functions for interacting with state:
const handleCriteriaColumnChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].column = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaOperatorChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].operator = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaValueChange = (index, value) => {
setCriteriaValue(value);
const newCriteria = [...currentRuleCriteria];
newCriteria[index].value = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaValueTypeChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].valueType = value;
setCurrentRuleCriteria(newCriteria);
setCriteriaValueType(value);
};
const handleAddCriteria = () => {
setCurrentRuleCriteria([...currentRuleCriteria, {
column: criteriaColumn,
operator: criteriaOperator,
value: criteriaValue,
valueType: criteriaValueType
}]);
setCriteriaColumn("title");
setCriteriaOperator("contains");
setCriteriaValue("");
setCriteriaValueType("text");
};
const handleRemoveCriteria = (index) => {
const newCriteria = [...currentRuleCriteria];
newCriteria.splice(index, 1);
setCurrentRuleCriteria(newCriteria);
};
The Problem:
I can see that when I add a new criteria a new set of input fields are added. Similarly, when I edit the values the state is always maintained correctly. The issue I am having is when deleting a pre-existing row of elements.
If I have two criteria in place, as in the image above, and I select the first one to be deleted - the state updates correctly, however the incorrect input fields seem to be deleted on the frontend and so it no longer aligns with the state values. In this example the following would then be visible:
I don't understand this as the input fields should be being generated by mapping over the currentRuleCriteria state value.
I have tried many things to try and rectify this, but can't seem to figure it out. I previously did not have the value specified on the individual select elements and was in the same situation as I am currently. Adding in a value field and referencing the state did not change anything and so I believe the select fields are not being populated by state values.
Any thoughts? Thank you!

You should set the key on TripleInputContainer:
<TripleInputContainer key={criteria.id}>
React docs says "Keys should be stable, predictable, and unique". But in your case there may be complete duplicates of the criteria. So it makes sense to use Math.random():
const [currentRuleCriteria, setCurrentRuleCriteria] = useState([
{
id: Math.random(),
column: "title",
operator: "contains",
value: "",
valueType: "text",
},
]);
setCurrentRuleCriteria([
...currentRuleCriteria,
{
id: Math.random(),
column: criteriaColumn,
operator: criteriaOperator,
value: criteriaValue,
valueType: criteriaValueType,
},
]);
It's even better to use nanoid and not worry about collisions.

Related

React select dropdowns that depend on each other

I am trying to make 3 select dropdowns automatically change based on the selection.
First dropdown has no dependencies, 2nd depends on first, and 3rd depends on 2nd.
This is a very simplified version of my code:
// record, onSave, planets, countries, cities passed as props
const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
const [selectedCountry, setSelectedCountry] = useState(record.country);
const [selectedCity, setSelectedCity] = useState(record.city);
const filteredCountries = countries.filter(c => c.planet === selectedPlanet.id);
const filteredCities = cities.filter(c => c.country === selectedCountry.id);
return (
<div>
<select value={selectedPlanet.id} onChange={(e) => setSelectedPlanet(e.target.value)}>
{planets.map(p => (
<option key={p.id} value={p.id} name={p.name} />
)}
</select>
<select value={selectedCountry.id} onChange={(e) => setSelectedCountry(e.target.value)}>
{filteredCountries.map(c => (
<option key={c.id} value={c.id} name={c.name} />
)}
</select>
<select value={selectedCity.id} onChange={(e) => setSelectedCity(e.target.value)}>
{filteredCities.map(c => (
<option key={c.id} value={c.id} name={c.name} />
)}
</select>
<button onClick={() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity ) }
</div>
);
The select options will update accordingly, but onSave() will receive outdated values for country and city if I select a planet and click the save button.
That is because setSelectedCountry and setSelectedCity are not called on planet change. I know I can just call them in that event, but it would make the code much uglier because I would have to duplicate the country and city filtering. Is there a better way around this?
Update
I updated the code example for the useReducer approach. It is functionally equivalent to the original example, but hopefully with cleaner logic and structure.
Live demo of updated example: stackblitz
Although it takes some extra wiring up, useReducer could be considered for this use case since it might be easier to maintain the update logic in one place, instead of tracing multiple events or hook blocks.
// Updated to use a common structure for reducer
// Also kept the reducer pure so it can be moved out of the component
const selectsReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "update_planet": {
const newCountry = payload.countries.find(
(c) => c.planet === payload.value
).id;
return {
planet: payload.value,
country: newCountry,
city: payload.cities.find((c) => c.country === newCountry).id,
};
}
case "update_country": {
return {
...state,
country: payload.value,
city: payload.cities.find((c) => c.country === payload.value).id,
};
}
case "update_city": {
return {
...state,
city: payload.value,
};
}
default:
return { ...state };
}
};
// Inside the component
const [selects, dispatchSelects] = useReducer(selectsReducer, record);
Original
While uglier or not is rather opinion-based, perhaps a potentially organized solution to handle the states dependent on each other is to use useReducer.
Live demo of below example: stackblitz
Here is a basic example that updates dependent values, such as if country is updated, then city will also change to the first available city in the new country to match it.
This keeps the value in the select lists updated, and it ensures onSave to always receive the updated values from the state.
const selectsReducer = (state, action) => {
const { type, planet, country, city } = action;
let newPlanet,
newCountry,
newCity = "";
switch (type) {
// 👇 Here to update all select values (the next 2 cases also run)
case "update_planet": {
newPlanet = planet;
}
// 👇 Here to update country and city (the next case also run)
case "update_country": {
newCountry = newPlanet
? countries.find((c) => c.planet === newPlanet).id
: country;
}
// 👇 Here to update only city
case "update_city": {
newCity = newCountry
? cities.find((c) => c.country === newCountry).id
: city;
return {
planet: newPlanet || state.planet,
country: newCountry || state.country,
city: newCity || state.city,
};
}
default:
return record;
}
};
const [selects, dispatchSelects] = useReducer(selectsReducer, record);
Since the dependency relationship is just in one direction, a simple approach would be just to clear everything that depends on the one that changed when it changes. So:
clear country and city when planet is selected
clear city when country is selected
<select value={selectedPlanet.id} onChange={(e) => setPlanetAndClearCountryAndCity(e)}>
{planets.map(p => (
<option key={p.id} value={p.id} name={p.name} />
)}
</select>
<select value={selectedCountry.id} onChange={(e) => setCountryAndClearCity(e)}>
{filteredCountries.map(c => (
<option key={c.id} value={c.id} name={c.name} />
)}
</select>
And put some validation that requires all 3 before save button is enabled.
Also you could consider not clearing the country or city if they actually still have valid options. But that seems like more work than it's worht.
You could use useEffect hook to handle select box updates. So that, you don't miss any update in values.
demo
export const SelectList = ({ record, onSave, planets, countries, cities }) => {
const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
const [selectedCountry, setSelectedCountry] = useState(record.country);
const [selectedCity, setSelectedCity] = useState(record.city);
/** handle filteredValues in state */
const [filteredCountries, setFilteredCountries] = useState([]);
const [filteredCities, setFilteredCities] = useState([]);
/** use useEffect to update values */
useEffect(() => {
const filteredCountries = countries.filter((c) => c.planet === selectedPlanet);
setFilteredCountries(filteredCountries);
setSelectedCountry(filteredCountries[0].id);
}, [selectedPlanet]);
useEffect(() => {
const filteredCities = cities.filter((c) => c.country === selectedCountry);
setFilteredCities(filteredCities);
setSelectedCity(filteredCities[0].id);
}, [selectedCountry]);
return (
<div>
<select value={selectedPlanet}
onChange={(e) => setSelectedPlanet(e.target.value)}>
{planets.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<select value={selectedCountry}
onChange={(e) => setSelectedCountry(e.target.value)}>
{filteredCountries.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<select
value={selectedCity}
onChange={(e) => setSelectedCity(e.target.value)}>
{filteredCities.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<button onClick={() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity })}>Save</button>
</div>
);
};
I think using React.useMemo hook for two variables: filteredCountries and filteredCities would be the best solution for you.
It will not make your code ugly and you don't have to filter twice.
Here is the updated code.
import * as React from 'react';
// record, onSave, planets, countries, cities passed as props
const [selectedPlanet, setSelectedPlanet] = useState(record.planet);
const [selectedCountry, setSelectedCountry] = useState(record.country);
const [selectedCity, setSelectedCity] = useState(record.city);
const filteredCountries = React.useMemo(() => {
const _countries = countries.filter(c => c.planet === selectedPlanet.id);
setSelectedCountry(_countries[0]);
return _countries;
}, [selectedPlanet]);
const filteredCities = React.useMemo(()=> {
const _cities = cities.filter(c => c.country === selectedCountry.id);
setSelectedCity(_cities[0]);
return _cities;
}, [setSelectedCountry])
return (
<div>
<select value={selectedPlanet.id} onChange={(e) => setSelectedPlanet(e.target.value)}>
{planets.map(p => (
<option key={p.id} value={p.id} name={p.name} />
)}
</select>
<select value={selectedCountry.id} onChange={(e) => setSelectedCountry(e.target.value)}>
{filteredCountries.map(c => (
<option key={c.id} value={c.id} name={c.name} />
)}
</select>
<select value={selectedCity.id} onChange={(e) => setSelectedCity(e.target.value)}>
{filteredCities.map(c => (
<option key={c.id} value={c.id} name={c.name} />
)}
</select>
<button onClick={() => onSave({planet: selectedPlanet, country: selectedCountry, city: selectedCity ) }
</div>
);
One way is to handle it by useEffect hook. this soloution is simple logic and obviously u can handle it with more details and conditions.
import React, { useEffect, useState } from "react";
const planets = [
{ id: 1, name: "earth", value: "earth" },
{ id: 2, name: "mars", value: "mars" },
{ id: 3, name: "jupiter", value: "jupiter" }
];
const countries = [
{ id: 1, name: "USA", value: "USA", planet: "earth" },
{ id: 2, name: "UAE", value: "UAE", planet: "mars" },
{ id: 3, name: "Canada", value: "Canada", planet: "mars" },
{ id: 4, name: "England", value: "England", planet: "earth" }
];
const cities = [
{ id: 1, name: "Los Angeles", value: "Los Angeles", country: "USA" },
{ id: 2, name: "Dubai", value: "Dubai", country: "UAE" },
{ id: 3, name: "Torento", value: "Torento", country: "Canada" },
{ id: 4, name: "Washington", value: "Washington", country: "USA" },
{ id: 5, name: "London", value: "London", country: "England" }
];
export default function MaterialUIPickers() {
const [selectedPlanet, setSelectedPlanet] = useState(planets[0].name);
const [selectedCountry, setSelectedCountry] = useState("");
const [selectedCity, setSelectedCity] = useState("");
const [countryItems, setCountryItems] = useState([]);
const [cityItems, setCityItems] = useState([]);
useEffect(() => {
setCountryItems([]); // reset select
setCityItems([]); // reset select
setSelectedCountry(""); // reset select
setSelectedCity(""); // reset select
selectedPlanet &&
setCountryItems(countries.filter((c) => c.planet === selectedPlanet));
}, [selectedPlanet]);
useEffect(() => {
setCityItems([]); // reset select
setSelectedCity(""); // reset select
selectedCountry &&
setCityItems(cities.filter((c) => c.country === selectedCountry));
}, [selectedCountry]);
return (
<div style={{ display: "flex-box", width: "vh" }}>
<select
value={selectedPlanet.id}
onChange={(e) => setSelectedPlanet(e.target.value)}
>
{planets.map((p) => (
<option key={p.id} value={p.name} name={p.name}>
{p.name}
</option>
))}
</select>
<select
value={selectedCountry?.id}
onChange={(e) => setSelectedCountry(e.target.value)}
>
{countryItems.map((c) => (
<option key={c.id} value={c.name} name={c.name}>
{c.name}
</option>
))}
</select>
<select
value={selectedCity?.id}
onChange={(e) => setSelectedCity(e.target.value)}
>
{cityItems.map((c) => (
<option key={c.id} value={c.name} name={c.name}>
{c.name}
</option>
))}
</select>
<button
onClick={() =>
console.log({
planet: selectedPlanet,
country: selectedCountry,
city: selectedCity
})
}
>
log selcets
</button>
</div>
);
}
https://codesandbox.io/s/kind-wilson-zpenc?file=/src/App.js
this is sandbox link

Setting Data in new Array to Map (React/Typescript)

I have data coming back from the db like so:
[{"id":"a7e45e4c-4af1-477e-9642-759fff49d44e","name":"Gallons","unitSize":[50,5,25]},
{"id":"5c607a2c-9388-4c09-a0dc-c6cd1d35a9c9","name":"lbs","unitSize":[30,90,60]}],
"errors":null}]
I have two dropdowns, one maps the Units of Measure (name) and its working great, I now want to create a new list for the second dropdown, which returns only the Unit Sizes (unitSize) for the selected Units Of Measure. The problem I am having is that react wont let me map through my list. Here is what it says:
UPDATE In my code, within the renderUnitSize function, my uosArr which is holding the specific data i want is working correctly, this is where I am now trying to set the data into uosList. Howeve I console.log it after I setState and my uoslist is empty. It is an array, however I am assuming this error is coming because its not holding any data?
public state = {
uosList: [],
};
public returnUnitSizes = () => {
const product = this.state.product;
const uom = product.unit;
const uosArr = this.state.unitOfMeasures.find(um => um.name === uom);
this.setState({
uosList: uosArr
});
console.log();
}
public handleUOMChange(event: any) {
const product = this.state.product;
product.unit = event.target.value;
this.setState({
product
}, () => this.returnUnitSizes());
console.log(product);
}
<FormGroup className="required">
<Label>Unit of Measure</Label>
<EInput
type="select"
name="unitOfMeasure"
id="unitOfMeasure"
value={this.state.product.unit}
onChange={this.handleUOMChange}
required={true}
>
<option />
{this.state.unitOfMeasures.map((UOM: IUnitOfMeasure,
index: number) =>
<option key={UOM.id} value={UOM.name}>{UOM.name}
</option>
)}
</EInput>
</FormGroup>
<FormGroup className="required">
<Label>Unit Size</Label>
<EInput
type="select"
name="unitOfMeasure"
value={this.state.product.size}
onChange={this.handleUnitSizeChange}
disabled={this.state.unitOfMeasure.id !== ''}
required={true}
>
<option />
{this.state.uosList && this.state.uosList.map((uosList: any, index: number) =>
<option key={index} value={uosList[index]}>{uosList}</option>
)
}
</EInput>
</FormGroup>
Your mistakes lies in this function:
public returnUnitSizes = () => {
const product = this.state.product;
const uom = product.unit;
const uosArr = this.state.unitOfMeasures.find(um => um.name === uom);
this.setState({
uosList: uosArr
});
console.log();
}
You are setting uosList to uosArr. But uosArr refers to an element of the array this.state.unitOfMeasures. As you wrote, this is an object with the shape:
{
id: string,
name: string,
unitSize: Array,
}
In reality you want to set uosList to uosArr.unitSize.

How to get a value from onChange in react.js?

I'm trying to get value from onChange using setState but for some reason when I write text on input I get an error like Axis.map is not a function
Also,I'd like to delete Axisdata singly from the last one using splice or pop but whenever I click the delete button the Axis data disappeared except the first one.
Set Elements
const SetElements = ({
...
}) => {
const [Axis, setAxis] = useState([]);
const AxisHandler = e => {
setAxis([
...Axis,
{
label: "",
data: "",
backgroundColor: "",
},
]);
};
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
return (
<>
<button onClick={AxisHandler}>add Line</button>
{Axis.length !== 1 && (
<button onClick={deleteAxis}>delete Line</button>
)}
{Axis.map((element, index) => (
<>
<AppendingAxis
Axis={Axis}
setAxis={setAxis}
element={element}
index={index}
/>
</>
))}
</>
)
AppendingAxis
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => setAxis((Axis[index].label = e.target.value))}
/>
</span>
The issue is state mutation in the AppendingAxis component.
onChange={e => setAxis((Axis[index].label = e.target.value))}
You should shallow copy state, and nested state, then update specific properties.
onChange={e => setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: e.target.value
}
: el,
)}
I'm not a fan of passing the state updater function on to children as this make it the child component's responsibility to maintain your state invariant. I suggest moving this logic into the parent component so it can maintain control over how state is updated.
SetElements parent
const changeHandler = index => e => {
const { value } = e.target;
setAxis(Axis => Axis.map((el, i) => i === index
? {
...el,
label: value
}
: el,
);
};
...
<AppendingAxis
Axis={Axis}
onChange={changeHandler(index)}
/>
AppendingAxis child
const AppendingAxis = ({ Axis, onChange }) => {
console.log(Axis);
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={onChange}
/>
</span>
And for completeness' sake, your delete handler looks to also have a mutation issue.
const deleteAxis = () => {
setAxis(Axis.splice(-1, 1));
};
.splice mutates the array in-place and returns an array containing the deleted elements. This is quite the opposite of what you want I think. Generally you can use .filter or .slice to generate new arrays and not mutate the existing one.
const deleteAxis = () => {
setAxis(Axis => Axis.slice(0, -1)); // start at 0, end at second to last
};
This is happening because of this line:
onChange={e => setAxis((Axis[index].label = e.target.value))}
Create a function:
const handleAxisChange = (e, index) => {
Axis[index].label = e.target.value;
setAxis(new Array(...Axis));
}
And then change set the onChange like this:
onChange={e => handleAxisChange(e, index)}
Your problem is because of you don't mutate state correctly. You should make a shallow copy of the state. You can change AppendingAxis to this code:
const AppendingAxis = ({
index,
setAxis,
Axis,
}) => {
console.log(Axis);
const onChange = (e,index)=>{
let copy = [...Axis];
copy[index].label = e.target.value;
setAxis(copy);
}
return (
<AxisSetting>
<h4>{index + 2}Y Axis setting</h4>
<span>
<input
placeholder={index + 2 + "setting"}
type="textarea"
onChange={e => onChange(e,index))}
/>
</span>

how to configure dynamic form fields inside a wizard when using a state machine

I am trying to implement a multi-step wizard using a state machine and am unsure how to handle some configurations. To illustrate this I put together an example of a wizard that helps you prepare a dish.
Assuming the following example what would be the appropriate way to model this form/wizard behavior as a state machine?
Step 1 - Dish
pick a dish from ["Salad", "Pasta", "Pizza"]
Step 2 - Preparation Method
pick a preparation method from ["Oven", "Microwave"]
Step 3 - Ingredients
add and select ingredients in a form, depending on the dish and the preparation method the form will look different
// ingredients based on previous selections
("Pizza", "Oven") => ["tomato", "cheese", "pepperoni"]
("Pizza", "Microwave") => ["cheese", "pepperoni", "mushrooms"]
("Pasta", "Oven") => ["parmesan", "butter", "creme fraiche"]
("Pasta", "Microwave") => ["parmesan", "creme fraiche"]
("Salad") => ["cucumber", "feta cheese", "lettuce"]
I tried to simplify the problem as much as possible. Here are my questions:
In step 3 I want to show a form with various fields of different types. The selections in step 1 and 2 define which fields will be shown in the form in step 3. What is the appropriate way to specify this form configuration?
Step 2 should be skipped if the selected dish from step 1 is "Salad". What is the appropriate way to declare this?
I plan to implement this using xstate as the project I'm working on is written in react.
Edit: I updated the example in reaction to Martins answer. (see my comment on his answer)
Edit 2: I updated the example in reaction to Davids answer. (see my comment on his answer)
For the overall flow, you can use guarded transitions to skip the method step if "salad" was selected:
const machine = createMachine({
initial: 'pick a dish',
context: {
dish: null,
method: null
},
states: {
'pick a dish': {
on: {
'dish.select': [
{
target: 'ingredients',
cond: (_, e) => e.value === 'salad'
},
{
target: 'prep method',
actions: assign({ dish: (_, e) => e.value })
}
]
}
},
'prep method': {
on: {
'method.select': {
target: 'ingredients',
actions: assign({ method: (_, e) => e.value })
}
}
},
'ingredients': {
// ...
}
}
});
And you can use the data-driven configuration from Matin's answer to dynamically show ingredients based on the context.dish and context.method.
You need to have a data structure that holds data and the relationship between them then you can use state to store the selected item and have your logic to display/hide specific step.
Below is just a simple example to show how you can do it:
Sandbox example link
const data = [
{
// I recommend to use a unique id for any items that can be selective
dish: "Salad",
ingredients: ["ingredient-A", "ingredient-B", "ingredient-C"],
preparationMethods: []
},
{
dish: "Pasta",
ingredients: ["ingredient-E", "ingredient-F", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
},
{
dish: "Pizza",
ingredients: ["ingredient-H", "ingredient-I", "ingredient-G"],
preparationMethods: ["Oven", "Microwave"]
}
];
export default function App() {
const [selectedDish, setSelectedDish] = useState(null);
const [selectedMethod, setSelectedMethod] = useState(null);
const [currentStep, setCurrentStep] = useState(1);
const onDishChange = event => {
const selecetedItem = data.filter(
item => item.dish === event.target.value
)[0];
setSelectedDish(selecetedItem);
setSelectedMethod(null);
setCurrentStep(selecetedItem.preparationMethods.length > 0 ? 2 : 3);
};
const onMethodChange = event => {
setSelectedMethod(event.target.value);
setCurrentStep(3);
};
const onBack = () => {
setCurrentStep(
currentStep === 3 && selectedMethod === null ? 1 : currentStep - 1
);
};
useEffect(() => {
switch (currentStep) {
case 1:
setSelectedDish(null);
setSelectedMethod(null);
break;
case 2:
setSelectedMethod(null);
break;
case 3:
default:
}
}, [currentStep]);
return (
<div className="App">
{currentStep === 1 && <Step1 onDishChange={onDishChange} />}
{currentStep === 2 && (
<Step2
onMethodChange={onMethodChange}
selectedMethod={selectedMethod}
selectedDish={selectedDish}
/>
)}
{currentStep === 3 && <Step3 selectedDish={selectedDish} />}
{selectedDish !== null && (
<>
<hr />
<div>Selected Dish: {selectedDish.dish}</div>
{selectedMethod !== null && (
<div>Selected Method: {selectedMethod}</div>
)}
</>
)}
<br />
{currentStep > 1 && <button onClick={onBack}> Back </button>}
</div>
);
}
const Step1 = ({ onDishChange }) => (
<>
<h5>Step 1:</h5>
<select onChange={onDishChange}>
<option value={null} disabled selected>
Select a dish
</option>
{data.map(item => (
<option key={item.dish} value={item.dish}>
{item.dish}
</option>
))}
</select>
</>
);
const Step2 = ({ onMethodChange, selectedMethod, selectedDish }) => (
<>
<h5>Step 2:</h5>
<div>
<select onChange={onMethodChange} value={selectedMethod}>
<option value={null} disabled selected>
Select a method
</option>
{selectedDish.preparationMethods.map(method => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</div>
</>
);
const Step3 = ({ selectedDish }) => (
<>
<h5>Step 3:</h5>
<h4>List of ingredient: </h4>
{selectedDish.ingredients.map(ingredient => (
<div key={ingredient}>{ingredient}</div>
))}
</>
);

Adding new columns dynamically with mui-datatable

I want to add a new column in mui-datatable every time a button is pressed. However datatables doesn't seem to be rendering it. However, if I clicked on the add column button, and then select the dropdown, the new columns appear. Furthermore the selected dropdown value does not seem to be reflected as well . I've narrowed the issue down to using const [columns,setColumns] = useState(...) for the column, but without that, I can't add any new columns dynamically at all. Appreciate any help to get me out of this pickle, thank you.
https://codesandbox.io/s/unruffled-sun-g77xc
const App = () => {
function handleChange(event) {
setState({ value: event.target.value });
}
const [state, setState] = useState({ value: "coconut" });
const [columns, setColumns] = useState([
{
name: "Column 1",
options: {}
},
{
name: "Column with Input",
options: {
customBodyRender: (value, tableMeta, updateValue) => {
return (
<div>
<select value={state.value} onChange={handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</div>
);
}
}
}
]);
const options = {
responsive: "scroll"
};
const addColumn = () => {
columns.push({
name: "NewColumn",
label: "NewColumn"
});
setColumns(columns);
};
const data = [["value", "value for input"], ["value", "value for input"]];
return (
<React.Fragment>
<MUIDataTable columns={columns} options={options} data={data} />
//add a new column if this button is clicked
<button onClick={addColumn}>Add Column</button>
</React.Fragment>
);
};
Your new column wasn't actually getting pushed to the columns variable. When you're using useState, you can't make changes to the variable unless you use setColumns, otherwise it won't trigger a rerender. Try this:
const addColumn = () => {
setColumns([ ...columns, {
name: "NewColumn"
}]);
};
Or this:
const addColumn = () => {
const editableColumns = [...columns];
editableColumns.push({
name: "NewColumn"
});
setColumns(editableColumns);
};
Both will work, it's just your preference.
You can test if it's editing the columns with this:
useEffect(() => console.log(columns), [columns]);

Resources