Hey I'm new to React and I'm having a problem with my prop which I'm passing to my ChildComponent. I am using "react-select" and have two multiselects. Originally I wanted to show in each select the value for the corresponding select I get from the state.
value={optionsColor.filter(item => ( myTest.color.includes(item.value)))}
But this is not possible because one of my calls is always "undefined". For example "myTest.Color" and "myTest.Car" one of them is "undefined" but I don't know why?
In my code (ChildComponent) I have two console.logs which illustrate this.
For example, if I select Color and have previously selected a car in Car, the console.log output looks like this.
undefined
blue
But I want it to output both.
import {useState} from "react";
import ChildComponent from "./ChildComponent";
const ParentComponent = () => {
const [step, setStep] = useState(0)
const [myTest, setMyTest] = useState(
{
color: ['defaultColor'],
car: ['defaultCar'],
}
)
const handleChange = (e, action) => {
setMyTest({ [action.name]: e ? e.map(x => x.value) : [] })
}
return (
<div>
<div className="card-body container mt-3">
<h2>Product data input Intended Use</h2>
<div className="card p-2 mt-5">
<form className="mb-4">
<div className="form"></div>
<ChildComponent myTest={myTest} handleChange={handleChange}/>
</form>
</div>
</div>
</div>
)
}
export default ParentComponent;
import React, { useState } from 'react';
import Select from 'react-select';
const optionsColor = [
{ value: 'blue', label: 'Blue' },
{ value: 'red', label: 'Red' },
{ value: 'yellow', label: 'Yellow' }
]
const optionsCar = [
{ value: 'bmw', label: 'BMW' },
{ value: 'ford', label: 'Ford' },
{ value: 'vw', label: 'VW' },
]
const ChildComponent = ({ handleChange, myTest}) => {
return (
<div>
<h4>Car {console.log(myTest.car)}</h4>
<Select
name="car"
options={optionsCar}
className="mb-3"
onChange={handleChange}
//value={intendedUse.sex === undefined ? '' : optionsSex.filter(item => (intendedUse.sex.includes(item.value)))}
isMulti
autoFocus
isSearchable
/>
<h4>Color {console.log(myTest.color)}</h4>
<Select
name="color"
options={optionsColor}
className="mb-3"
onChange={handleChange}
//value={intendedUse.age === undefined ? '': optionsAge.filter(item => ( intendedUse.age.includes(item.value)))}
isMulti
autoFocus
isSearchable
/>
</div>
)
}
export default ChildComponent;
the problem lies here.
setMyTest({ [action.name]: e ? e.map(x => x.value) : [] })
When you're updating your myTest state you're actually replacing both of the fields with the field you're setting.
Try something like this:
setMyTest(myTest => ({...myTest, { [action.name]: e ? e.map(x => x.value) : [] }}));
In that way, you have a new object with both the field that changed and the one that didn't.
Related
what i am creating here is a sorter with 3 inputs like shown here . This sorter will get some data from a table. Now i'm setting state with initSelect and i'm passing it the fields array but when i console.log(select) it gives me the object shown in the image which is incorrect from the behaviours i want {sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
The first input has to have a default value of 'Date' always , but it can change to other values in the dropdown list like name , username ect . When i console log the select state it is messed up as it's always selecting the last one on the fields array , how can i change the initSelects function to correctly build the object i want.
Also the tricky thing which i can't seem to do is , if this Date value is selcted , in the second input, the date value should not be there. And if in the second input we select another value like Earth , Earth and Date should not be in the 3rd input and so on. So basically it means filtering out values . I need serious help as this is for the company i work on
Excepted Behaviour: Dynamically update value every time i select one input element like
{sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
When selectin 'Date' for example , it shouldn't not be included in the dropdownlist on sorterParam2, sorterParam3.
/*eslint-disable*/
import React, { useState, useMemo } from 'react';
import TextField from '#mui/material/TextField';
import Button from '#mui/material/Button';
import { GridSortModel } from '#mui/x-data-grid';
import SorterField from './SorterField';
const initSelects = (fields) => {
let object = {};
fields.map((item) => {
console.log(item, 'item');
object = {
...item,
[item.name]: item.defaultValue ? item.defaultValue : '',
};
});
return object;
};
const Sorter = ({ menuItemsValue, setSortData }: SortProps) => {
const fields: SorterProps[] = [
{
name: 'sorterParam1',
title: 'Sort by',
optional: false,
defaultValue: 'Date',
},
{
name: 'sorterParam2',
title: 'Then by',
optional: true,
},
{
name: 'sorterParam3',
title: 'Then by',
optional: true,
},
];
const [select, setSelect] = useState<any>(() => initSelects(fields));
const getMenuItems = useMemo(() => {
return menuItemsValue.filter((item) => select.sorterParam1 !== item);
}, [menuItemsValue, select]);
const handleSelectChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
console.log(111, name, value);
setSelect({ ...select, [name]: value });
setSortData(sortOptions);
};
const handleClearAllInputs = () => {
setSelect({
sorterParam1: '',
sorterParam2: '',
sorterParam3: '',
});
};
const handleConfirm = () => {};
return (
<TextField
label="Advanced Sorting"
className={styles.sorter__inputs}
id="sorter-parameter-1"
variant="standard"
InputProps={{
disableUnderline: true,
}}
select
SelectProps={{
IconComponent: (props) => <NewIcon {...props} />,
}}
sx={{
fontSize: '12px',
width: '100%',
'& .MuiInputBase-input:focus': {
backgroundColor: 'transparent !important',
},
'& .MuiInputLabel-root': {
color: '#9E9E9E',
},
'& .MuiTextField-root': {
fontSize: '13px',
},
'& .MuiOutlinedInput-root': {
backgroundColor: '#fff',
},
}}
>
{fields.map((option, index) => (
<SorterField
key={option.name}
menuItemsValue={getMenuItems}
name={option.name}
option={option}
count={fields.length}
handleChange={handleSelectChange}
index={index + 1} // setData={setData}
/>
))}
<div className={styles.sorter__inputControllers}>
<Button
className={styles.sorter__clearAllInput}
onClick={() => handleClearAllInputs()}
>
Clear All
</Button>
<Button
onClick={() => handleConfirm()}
className={styles.sorter__confirmInput}
>
Confirm
</Button>
</div>
</TextField>
);
};
export default Sorter;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
This is the SorterField Component code if that might be helpful
/*eslint-disable*/
import React, { useState } from 'react';
import TextField from '#mui/material/TextField';
import { MenuItem } from '#mui/material';
import { SorterProps } from '../../types/Sorter';
import { ReactComponent as SorterLine } from '../../assets/img/sortLine.svg';
import styles from '../../assets/components/Sorter/sorter.module.scss';
type SorterFieldProps = {
menuItemsValue: string[];
option: SorterProps;
count: number;
name: string;
handleChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
index: number;
};
function SorterField({
option,
count,
menuItemsValue,
handleChange,
index,
}: SorterFieldProps) {
const handleSorting = () => {};
return (
<div className={styles.sorter__container}>
<div className={styles.sorter__header}>
<p className={styles.sorter__label}>
{option.title}{' '}
{option.optional && (
<sup className={styles.sorter__optional}>*Optional</sup>
)}
</p>
<div className={styles.sorter__numbers__container}>
{Array.from({ length: count }, (_, i) => i + 1).map((number) => (
<>
{number === index ? (
<>
<span className={styles.sorter__number}>{index}</span>
</>
) : (
<>
<span className={styles.sorter__numbers}>{number}</span>
</>
)}
</>
))}
</div>
</div>
<div className={styles.sorter__inputs}>
<TextField
className={[styles.sorter__input, styles.sorter__field__input].join(
' '
)}
variant="outlined"
label="Select"
select
SelectProps={{
IconComponent: () => <NewIcon />,
}}
value={option.defaultValue}
onChange={handleChange}
name={option.title}
size="small"
>
{menuItemsValue.map((title, idx) => (
<MenuItem key={idx} value={title}>
{title}
</MenuItem>
))}
</TextField>
<div onClick={handleSorting} className={styles.sorter__sortOrder}>
<div className={styles.sorter__sortOrder__alphabetical}>
<span>A</span>
<span>Z</span>
</div>
<div className={styles.sorter__sortOrder__sortLine}>
<SorterLine />
</div>
</div>
</div>
</div>
);
}
export default SorterField;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You're re-assigning a value to object in each iteration of fields.map().
If you want to build object as a map of field item name to defaultValue (or blank string), use this instead...
return Object.fromEntries(
fields.map(({ name, defaultValue }) => [name, defaultValue ?? ""])
);
See Object.fromEntries()
Also, move the fields declaration outside your component. It's static content so can be omitted from being declared every render and from any hook dependencies.
You could also use fields.reduce() which is basically the same thing
return fields.reduce(
(obj, { name, defaultValue }) => ({
...obj,
[name]: defaultValue ?? "",
}),
{}
);
As for removing selected options as you iterate through the fields, that's a little trickier.
You could use a memo hook to create an iterable data structure that includes the available options for that particular iteration.
For example
const fieldsWithOptions = useMemo(() => {
const taken = new Set(Object.values(select));
const available = menuItemsValue.filter((item) => !taken.has(item));
return fields.map((field) => ({
...field,
options: select[field.name]
? [select[field.name], ...available] // include the current selection
: available,
}));
}, [menuItemsValue, select]);
Then map over fieldsWithOptions instead of fields and use option.options instead of getMenuItems.
I know, this topic has been handeled a lot, but I am still lost in my particular example. I have a react-select component, which is a part of another component, which is a part of App component.
SubjectSelect.tsx
export default function SubjectSelect({handleChange, value}) {
return (
<>
<Select
placeholder="Choose subject"
value={value} // set selected value
onChange={handleChange} // assign onChange function
/>
<div><b>Your choice: </b> {value} </div>
</>
)
}
FormStepOne.tsx
import SubjectSelect from "../components/SubjectSelect";
export const SubjectSelector = ({value, handleChange}) => {
return (
<>
<h1 className="text-3xl font-bold underline">
Hi there! Please select book subject
</h1>
<SubjectSelect value={value} onChange={handleChange}/>
</>
);
}
And App.tsx
import React, { useState } from 'react'
import { SubjectSelector } from '../formSteps/stepOne'
import { ShowcaseBooks } from '../formSteps/stepTwo'
const options = [
{ value: 'fiction', label: 'Fiction' },
{ value: 'science', label: 'Science' },
]
export default function App() {
const [books, setBooks] = useState('')
const [selectedValue, setSelectedValue] = useState('');
const handleChange = e => {
setSelectedValue(e.value);
alert('huhu')
}
const value = options.find(obj => obj.value === selectedValue)
return (
<div className="bg-blue-200">
<div className="container mx-auto py-36 bg-blue-200">
<div className="mt-12 px-96">
<SubjectSelector
options={options}
value={value}
onChange={handleChange}/>
<ShowcaseBooks books={books}/>
</div>
</div>
</div>
)
}
Somehow I am not passing the props right, so that my components showcase several errors. App.tsx complains about options and onChange, but I am lost and don't reallyfully undertand what is wrong and how to pass props corretly, so my App.js and therefore Showcase books "know" the selectdValue.
The problem is you pass the handleChange function to SubjectSelector on the onChange prop, while that component is expecting it on a prop named handleChange.
In App.tsx you need something like this
<SubjectSelector
options={options}
value={value}
handleChange={handleChange}/> // This is the fix
Because when you do this
export const SubjectSelector = ({value, handleChange}) =>
You're telling the component the name of the props to expect. You need to change your Subject selector in a similar manner.
For first loading, the prop value you send is empty (because change event is not triggered). so It will turn your app into error.
So you need to check it not empty before parse it to HTML
SubjectSelect.tsx
export default function SubjectSelect({ options, handleChange, selectedVal }) {
return (
<>
<select
value={selectedVal && selectedVal.value} // set selected value
onChange={handleChange} // assign onChange function
>
<option>Choose subject</option>
{options.map((item: any) => {
return (
<option key={item.value} value={item.value}>
{item.label}
</option>
);
})}
</select>
<div>
<b>Your choice: </b> {selectedVal && selectedVal.value}
</div>
</>
);
}
FormStepOne.tsx
import SubjectSelect from "./SubjectSelect";
const SubjectSelector = ({ selectedVal, handleChange, options }) => {
return (
<>
<h1 className="text-3xl font-bold underline">
Hi there! Please select book subject
</h1>
<SubjectSelect
options={options}
selectedVal={selectedVal}
handleChange={handleChange}
/>
</>
);
};
export default SubjectSelector;
And do not use value as variable name or prop to avoid conflict with key of object.
Getting value from change event should be e.target.value
import React, { useState } from "react";
import SubjectSelector from "./SubjectSelect";
const options = [
{ value: "fiction", label: "Fiction" },
{ value: "science", label: "Science" }
];
export default function App() {
const [selectedValue, setSelectedValue] = useState('');
const handleChange = (e: any) => {
setSelectedValue(e.target.value);
alert(e.target.value);
};
const selectVal = options.filter((obj) => obj.value === selectedValue)[0];
return (
<div className="bg-blue-200">
<div className="container mx-auto py-36 bg-blue-200">
<div className="mt-12 px-96">
<SubjectSelector
options={options}
selectedVal={selectVal}
handleChange={handleChange}
/>
</div>
</div>
</div>
);
}
You can check my complete answer here. I hope it might helps you
https://codesandbox.io/s/delicate-smoke-n0eyjc?file=/src/App.tsx:0-804
//value={items.find(x => x.item === selectedValue)}import { Fragment, useState } from //"react";
import Select from 'react-select';
let items = [
{
item: 1,
name: "tv"
},
{
item: 2,
name: "PC"
}
]
const Home = () => {
const [selectedValue, setSelectedValue] = useState(6)
const handleChange = obj => {
setSelectedValue(obj.item)
}
return (
<Fragment>
<div>Home page</div>
<p>Test React Select...</p>
<Select
value={items.find(x => x.item === selectedValue)}
options={items.map(({item, name}) => ({value: name, label: item}))}
onChange={handleChange}
/>
<p>selected Value:...</p>
{selectedValue}
</Fragment>
)
}
export default Home;
It would be helpful if you could provide more information, like if you're getting an error.
However, I think your problem is twofold:
the value of your Select component is being set to the entire object instead of the value.
You've mixed up the value and label keys in the options prop.
Try this:
<Select
value={items.find(x => x.item === selectedValue).item}
options={items.map(({item, name}) => ({value: item, label: name}))}
onChange={handleChange}
/>
EDIT: You don't need to use find, you already have this value:
<Select
value={selectedValue}
options={items.map(({item, name}) => ({value: item, label: name}))}
onChange={handleChange}
/>
I'm trying to build an input form for a project I'm working that submits user data to a database. some of the input fields that are required have over 100 selections that user chooses from. So I wanted to separate those data choices into their own files and them pass props to a select input group to dynamically generate those options. But I'm running into an issue when I try to declare my input component on my form page. I'm getting cannot read property 'map' of undefined. I think its because I'm declaring the input component in the choices file and also the form file. I'll supply the code below so it makes a little more sense.
This is the code that supplies the array data to the input. Its a very long file so ill only post part of it
const BuildingChoices = () => {
const buildingArray = [
{
id: 1,
name: "ACB",
value: "ACB"
},
{
id: 2,
name: "ADH",
value: "ADH"
},
{
id: 3,
name: "AHG",
value: "AHG"
},
{
id: 4,
name: "ANB",
value: "ANB"
},
{
id: 5,
name: "AND",
value: "AND"
},
{
id: 6,
name: "ARC",
value: "ARC"
},
{
id: 175,
name: "WIN",
value: "WIN"
},
{
id: 176,
name: "WMB",
value: "WMB"
},
{
id: 177,
name: "WPR",
value: "WPR"
},
{
id: 178,
name: "WWH",
value: "WWH"
}
]
return (
<SelectInput arrayData={buildingArray} />
)
}
export default BuildingChoices
Here is the select input field that where the options get dynamically generated based on the array passed to it.
import React from 'react'
const SelectInput = (props) => {
return (
<div className="input-group mb-3">
<label className="input-group-text" for="inputGroupSelect01">{props.dataName}</label>
<select onChange={props.onChange} value={props.value} name={props.name} className="form-select" id="inputGroupSelect01">
<option disabled>Choose...</option>
{props.dataArray.map(data => (
<option key={data.id} value={data.value}>{data.name}</option>
))}
</select>
</div>
)
}
export default SelectInput
And then this is the component where the input gets rendered onto the page. It's at the very bottom of the code. I'm pretty sure this is my problem because I'm declaring the in multiple files.
import React, { useState } from 'react'
import SelectInput from "../SelectInput"
const Prjt_Metadata_Form = () => {
const [inputs, setInputs] = useState({
project_id: '',
building: 'Choose...',
measure_type: '',
status: '',
staff_lead: '',
staff_colead: '',
analyst: '',
project_description: '',
nonenergy_benefits: '',
baseline_start_date: '',
reporting_period_start_date: '',
length_baseline_period_days: '',
length_reporting_period_days: ''
});
const { project_id, building, measure_type, status, staff_lead, staff_colead, analyst, project_description,
non_energy_benefits, baseline_start_date, reporting_period_start_date, length_baseline_period_days, length_reporting_period_days } = inputs
const onChange = e => {
const { name, value } = e.target
setInputs({
...inputs,
[name]: value
})
}
const onSubmit = async (e) => {
e.preventDefault();
try {
const body = {
project_id, building, measure_type, status, staff_lead, staff_colead, analyst, project_description,
non_energy_benefits, baseline_start_date, reporting_period_start_date, length_baseline_period_days, length_reporting_period_days
}
const response = await fetch('http://localhost:5000/api/prjt_metadata', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
window.location = '/'
console.log(response)
} catch (error) {
console.error(error.message);
};
}
return (
<div style={{ minHeight: '45vh' }} className="container d-flex align-items-center mt-5">
<div className="card border-secondary bg-secondary text-center mb-3" >
<div className="card-body text-black">
<form onSubmit={onSubmit}>
<div className="row">
<div className="col-md-4">
<div className="input-group mb-3">
<div className="input-group-prepend">
<span className="input-group-text" id="inputGroup-sizing-lg">Project ID</span>
</div>
<input type="text" name="project_id" className="form-control" value={project_id} onChange={onChange} />
</div>
<SelectInput
onChange={onChange}
value={building}
name="building"
dataName="Building"
/>
Any suggestions on how I can get this working are very appreciated! Thanks!
The problem
Look at how you render your SelectInput:
<SelectInput
onChange={onChange}
value={building}
name="building"
dataName="Building"
/>
Then in SelectInput you map over a prop dataArray which you do not pass as a prop.
props.dataArray.map(data => (
<option key={data.id} value={data.value}>{data.name}</option>
)
Alternative solution
If you don't the SelectInput to be flexible regarding the data used, you can outsource your buildingChoices as a constant to a separate file data.js and include it in your SelectInput to map over it. The relevant parts in your SelectInput.js should then look something like this:
import { buildingChoices } from 'data.js';
buildingChoices.map(data => (
<option key={data.id} value={data.value}>{data.name}</option>
)
However, you could also pass the buildingChoices constant as a prop when rendering `SelectInput
import { buildingChoices } from 'data.js';
<SelectInput
dataArray={buildingChoices}
onChange={onChange}
value={building}
name="building"
dataName="Building"
/>
Your SelectInput component is mapping from props.dataArray but in your Prjt_Metadata_Form component your SelectInput is missing the dataArray
Maybe with a High Order Component approach (HOC)?
const BuildingChoices = (Component) = (props) => {
const buildingArray = [
// { ... }
];
return (
<Component arrayData={buildingArray} {...props} />
)
}
export default BuildingChoices(SelectInput);
Then you can use it like this
<BuildingChoices
onChange={onChange}
value={building}
name="building"
dataName="Building"
/>
Im new to hook and so is react,Ive been watching some tutorials lately,I saw Ben awad's video for dynamic forms and I tried replicating it.There he used a callback inside the useState updater function which seems new to me.He used link setPeople(currentPeople => {}) What is the argument currentPeople come from and why its used,Can you someone please explain,Thanks in advance!
import { useState } from "react";
import "./App.css";
import { generate } from "shortid";
interface Person {
id: string;
firstName: string;
lastName: string;
}
function App() {
const [people, setPeople] = useState<Person[]>([
{
id: "5",
firstName: "Aashiq",
lastName: "Ahmed",
},
]);
return (
<>
<h2 style={{ textAlign: "center" }}>Dynamic Form </h2>
<div style={{ textAlign: "center" }}>
<button
onClick={() => {
setPeople((currentPeople) => [
...currentPeople,
{
id: generate(),
firstName: "",
lastName: "",
},
]);
}}
>
add new person
</button>
{people.map((p, index) => {
return (
<div key={p.id}>
<input
placeholder="first name"
value={p.firstName}
onChange={(e) => {
const firstName = e.target.value;
setPeople((
currentPeople
) =>
currentPeople.map((x) =>
x.id === p.id ? { ...x, firstName } : x
)
);
}}
/>
<input
placeholder="last name"
value={p.lastName}
onChange={(e) => {
const lastName = e.target.value;
setPeople((currentPeople) =>
currentPeople.map((x) =>
x.id === p.id ? { ...x,lastName } : x
)
);
}}
/>
<button onClick={()=> setPeople( currentPeople =>currentPeople.filter(x=> x.id !== p.id) )}>x</button>
</div>
);
})}
<div>
<pre>{JSON.stringify(people, null, 3)}</pre>
</div>
</div>
</>
);
}
export default App;
No better explanation than the official one.
Here is the link: https://reactjs.org/docs/hooks-reference.html#usestate
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Your currentPeople is what the variable name suggests, the current value of the
const [people, setPeople] = useState<Person[]
For example:
You might send only one person that you want to add to your people's array, so you end up just attaching that Person to an already existing array of Persons.
Sure you could use setPeople([...people, newPerson]) but this wouldn't be correct in 100% of places because people might not have the latest value so the callback function comes to the rescue.
It is the current value of that state, this might come in handy, when you want to use the previous state to calculate the next state. An example would be a toggle function, that would toggle a modal or sth.
const [isOpen, setIsOpen] = useState(false);
const toggleIsOpen = () => setIsOpen(open=>!open);