I'm using materiel UI form component in a modal.
This modal can be opened for add or edit an item, so values can be empty or not.
I put default props values in a state but this is always empty and never get previous values...
Here is my code :
const Comp = (props) => {
const { edit, values } = props // edit props for editing user
// values is :
{
prenom: 'name',
nom: 'name'
}
// ...
const [nom, setNom] = React.useState(edit ? values.nom : '')
const [prenom, setPrenom] = React.useState(edit ? values.prenom : '')
// ...
return (
<form>
<TextField
id="prenom"
value={prenom}
label="Prénom"
variant="outlined"
onChange={(event) => setPrenom(event.target.value)}
/>
<TextField
id="nom"
value={nom}
label="Nom"
variant="outlined"
onChange={(event) => setNom(event.target.value)}
/>
</form>
)
}
Thanks for your help
I'm guessing that you have your Comp used on the parent but not visible till some state changes, something like isDialogOpen. Then once the user wants to edit some object you do something like
setIsDialogOpen(true);
setDialogEditMode(true);
setValuesToEdit({nom: 'Foo', prenom: 'Bar'});
You have to understand that once you use the component (<Comp prop='value' />) React renders it, even that nothing gets to the actual Dom, the Comp function will be called! so at first it's being called with some empty values, then you want to let the user do the editing, you change the parent state. BUT the Comp state is still the state that it was created with (Empty values).
Remember this: useState acts in two ways:
On the first render it returns the value from the given parameter.
Any future renders it ignores the parameter and returns the saved state.
So in order to change the saved state you have to declare a reaction/effect when the props change, you do that with useEffect inside your Comp
for example:
useEffect(() => {
setNom(values.nom);
setPrenom(values.prenom);
}, [values])
Related
I have a Parent component which generates (and renders) a list of Child components like this:
const Parent= ({ user }) => {
const [state, setState] = useState ({selectedGames: null
currentGroup: null})
...
let allGames = state.selectedGames ? state.selectedGames.map(g => <Child
game={g} user={user} disabled={state.currentGroup.isLocked && !user.admin} />) : null
...
return ( <div> {allGames} </div>)
}
The Child component is also a stateful component, where the state (displayInfo) is only used to handle a toggle behaviour (hide/display extra data):
const Child = ({ game, user, disabled = false }) => {
const [displayInfo, setDisplayInfo] = useState(false)
const toggleDisplayInfo = () =>
{
const currentState = displayInfo
setDisplayInfo(!currentState)
}
...
<p className='control is-pulled-right'>
<button class={displayInfo ? "button is-info is-light" : "button is-info"} onClick = {toggleDisplayInfo}>
<span className='icon'>
<BsFillInfoSquareFill />
</span>
</button>
</p>
...
return ({displayInfo ? <p> Extra data displayed </p> : null})
}
When state.selectedGames is modified (for example when a user interacts with the Parent component through a select), state.selectedGames is correctly updated BUT the state of the i-th Child component stay in its previous state. As an example, let's say:
I clicked the button from the i-th Child component thus displaying "Extra data displayed" for this i-th Child only
I interact with the Parent component thus modifying state.selectedGames (no common element between current and previous state.selectedGames)
I can see that all Child have been correctly updated according to their new props (game, user, disable) but the i-th Child still has its displayInfo set to true (its state has thus not been reset contrary to what I would have (naively) expected) thus displaying "Extra data displayed".
EDIT:
After reading several SO topics on similar subjects, it appears using a key prop could solves this (note that each game passed to the Child component through the game props has its own unique ID). I have also read that such a key prop is not directly expose so I'm a bit lost...
Does passing an extra key prop with key={g.ID} to the Child component would force a re-rendering?
Oky, so wasted like 12 hours already on this. And I really need help.
I created a filter using an array via Map that returns some checkbox components
Component:
import { useEffect, useState } from "react"
interface Checkbox {
id: string | undefined,
name: string,
reg: any,
value: any,
label: string,
required?: boolean,
allChecked: boolean
}
const Checkbox = ({ id, name, reg, value, label, allChecked }: Checkbox) => {
const [checked, setChecked] = useState(false);
useEffect(() => {
setChecked(allChecked);
}, [allChecked])
return (
<>
<label key={id}>
<input type="checkbox"
{...reg}
id={id}
value={value}
name={name}
checked={checked}
onClick={() => {
setChecked(!checked)
}}
onChange={
() => { }
}
/>
{label}
</label>
</>
)
}
export default Checkbox;
Map:
dataValues.sort()
?
dataValues.map((e: any, i: number) => {
let slug = e.replace(' ', '-').toLowerCase();
return (
<div key={JSON.stringify(e)} id={JSON.stringify(e)}
>
<Checkbox id={slug}
name={title as string}
label={e as string}
value={e}
reg={{ ...register(title as string) }}
allChecked={allChecked}
/>
</div>
)
})
:
null
}
State that is just above the Map:
const [allChecked, setAllChecked] = useState<boolean>(false);
When I try to change the state on the parent and check or uncheck all of the checkboxes, nothing happens.
(the form works without a problem if I manually click on the checkboxes, but I cannot do this as I have some sections with over 40 values)
Sample of array:
dataValues = [
"Blugi",
"Bluza",
"Body",
"Bratara",
"Camasa",
"Cardigan",
"Ceas",
"Cercel",
"Colier",
"Fusta",
"Geanta Cross-body",
"Hanorac",
"Jacheta",
"Palton",
"Pantaloni",
"Pulover",
"Rochie",
"Rucsac",
"Sacou",
"Salopeta",
"Set-Accesorii",
"Top",
"Trench",
"Tricou",
"Vesta"
]
allChecked never changes (at least in the code shown here).
Here's the timeline:
The parent passes down a boolean prop allChecked. That's supposed to tell us if all the checkboxes are checked or not.
useEffect in Checkbox runs on mount, and allChecked is false because that's its default. useEffect then sets checked to allChecked's value, false. Which it already is, because its default is also false.
useEffect then listens for a change in allChecked via [allChecked] that never comes.
Clicking on any checkbox just toggles the state of that checkbox. allChecked's setter, setAllChecked, is never passed to the child or called from the parent.
What's the solution?
Somewhere, setAllChecked(true) needs to happen. Maybe a single checkbox with a label "Check all"?
Then, allChecked in Checkbox needs to be able to control the checkbox inputs. One implementation could be:
checked={checked || allChecked}
I managed to solve this.
There were 2 main problems:
Checkboxes were not rendering again so react-hook-form would see the old value, no matter what
I couldn't press clear all multiple times, because I was sending the allChecked = = false info multiple times, and the state wasn't exactly changing.
What I did was force a render by integrating the allChecked state as an object
interface checkedState {
checked: boolean,
render: boolean
}
So, whenever I send the state, I send it something like:
{
checked: true,
render: !allChecked.render
}
meaning that the new object is always new, no matter if I send the same information.
I am using a Material UI Select Component to render a simple drop down menu, with its value as a state declares using the useState method.
const [collaboratingTeams, setCollaboratingTeams] = useState([])
The below code is of the Select Component, with its value and the corresponsing handler function in its onChange prop.
<Select
validators={["required"]}
errorMessages={["this field is required"]}
select
multiple
variant="outlined"
value={collaboratingTeams}
name="collaboratingTeams"
onChange={(e) => handleSelectCollaboratingTeams(e)}
helperText="Select Collaborating Teams "
>
{arrTeams.map((option, index) => (
<MenuItem
key={option.teamId}
value={option.teamId}
variant="outlined"
>
<Checkbox
checked={collaboratingTeams.indexOf(option.teamId) !== -1}
/>
<ListItemText primary={option.teamValue} />
</MenuItem>
))}
</Select>
The below code is the function that triggers when a drop down data is changed.
This function sets the state, which should then technically update the Select's selected options.
const handleSelectCollaboratingTeams =(e)=>{
setCollaboratingTeams(e.target.value)
}
The issue is, the setCollaboratingTeams method isn't updating the state only. I understand that the setstate method in hooks works so coz of its asynchronous nature but at some point it should display up right. Don't understand where I'm going wrong.
I expect the collaboratingTeams array to be updated with a new value when a new value is selected by the user.
you should define the new state for storing the selected item.
Example for class component:
state = {
selectedOption: null,
};
handleChange = selectedOption => {
this.setState({ selectedOption });
};
Example for functional component(using React-hook):
const [selectedOption, setSelectedOption] = useState(null);
handleChange = selectedOption => {
setSelectedOption(selectedOption);
};
dont use arrow function with onchange it often used when we need to pass id or some other data
I have a React web app that is effectively a ton of Questions. These questions need to be validated/laid-out based on their own state values (ie: must be a number in a number field), as well as on the values of each other. A few examples of the more complex 'validation':
Questions A, B, and C might be required to have non-empty values before allowing a 'save' button.
Question B's allowable range of values might be dependent on the value of question A.
Question C might only show if question A is set to 'true'.
You can imagine many other interactions. The app has hundreds of questions - as such, I have their configuration in a JSON object like this:
{ id: 'version', required: true, label: 'Software Version', xs: 3 },
{
id: 'licenseType', label: 'License Type', xs: 2,
select: {
[DICTIONARY.FREEWARE]: DICTIONARY.FREEWARE,
[DICTIONARY.CENTER_LICENSE]: DICTIONARY.CENTER_LICENSE,
[DICTIONARY.ENTERPRISE_LICENSE]: DICTIONARY.ENTERPRISE_LICENSE
}
},
... etc.
I would then turn this object into actual questions using a map in the FormPage component, the parent of all the questions. Given the need to store these interaction in the closest common parent, I store all of the Question values in a formData state variable object and the FormPage looks like so:
function FormPage(props) {
const [formData, setFormData] = useState(BLANK_REQUEST.asSubmitted);
const handleValueChange = (evt, id) => {
setFormData({ ...formData, [id]: evt.target.value})
}
return <div>
{QUESTIONS_CONFIG.map(qConfig => <Question qConfig={qConfig} value={formData[qConfig.id]} handleValueChange={handleValueChange}/>)}
// other stuff too
</div>
}
The Question component is basically just a glorified material UI textField that has it's value set to props.value and it's onChange set to props.handleValueChange. The rest of the qConfig object and Question component is about layout and irrelevant to the question.
The problem with this approach was that every keypress results in the formData object changing... which results in a re-render of the FormPage component... which then results in a complete re-render/rebuild of all my hundreds of Question components. It technically works, but results performance so slow you could watch your characters show up as you type.
To attempt solve this, I modified Question to hold it's own value in it's own state and we no longer pass formData to it... the Question component looking something like this:
function Question(props) {
const { qConfig, valueChangedListener, defaultValue } = props;
const [value, setValue] = useState(props);
useEffect(() => {
if (qConfig.value && typeof defaultValue !== 'undefined') {
setValue(qConfig.value);
}
}, [qConfig.value])
const handleValueChange = (evt, id) => {
setValue(evt.target.value);
valueChangedListener(evt.target.value, id)
}
return <div style={{ maxWidth: '100%' }}>
<TextField
// various other params unrelated...
value={value ? value : ''}
onChange={(evt) => handleValueChange(evt, qConfig.id)}
>
// code to handle 'select' questions.
</TextField>
</div>
}
Notably, now, when it's value changes, it stores it's own value only lets FormPage know it's value was updated so that FormPage can do some multi-question validation.
To finish this off, on the FormPage I added a callback function:
const processValueChange = (value, id) => {
setFormData({ ...formData, [id]: value })
};
and then kept my useEffect that does cross-question validation based on the formData:
useEffect(() => { // validation is actually bigger than this, but this is a good example
let missingArr = requiredFields.filter(requiredID => !formData[requiredID]);
setDisabledReason(missingArr.length ? "Required fields (" + missingArr.join(", ") + ") must be filled out" : '');
}, [formData, requiredFields]);
the return from FormPage had a minor change to this:
return <div>
{questionConfiguration.map(qConfig =>
<Question
qConfig={qConfig}
valueChangedListener={processValueChange}
/>
</ div>
)
}
Now, my problem is -- ALL of the questions still re-render on every keypress...
I thought that perhaps the function I was passing to the Question component was being re-generated so I tried wrapping processValueChange in a useCallback:
const processValueChange = React.useCallback((value, id) => {
setFormData({ ...formData, [id]: value })
}
},[]);
but that didn't help.
My guess is that even though formData (a state object on the FormPage) is not used in the return... its modification is still triggering a full re-render every time.
But I need to store the value of the children so I can do some stuff with those values.
... but if I store the value of the children in the parent state, it re-renders everything and is unacceptbaly slow.
I do not know how to solve this? Help?
How would a functional component store all the values of its children (for validation, layout, etc)... without triggering a re-render on every modification of said data? (I'd only want a re-render if the validation/layout function found something that needed changing)
EDIT:
Minimal sandbox: https://codesandbox.io/s/inspiring-ritchie-b0yki
I have a console.log in the Question component so we can see when they render.
We currently have a component <RenderDropdown /> which renders a material-ui <Select /> element. This takes options as props and is wrapped in a react-final-form <Field />.
Our issue is when we load the initialValues props of the <Form />, there are a few cases where the selected dropdown value no longer exists in the dropdown, so we want to set the dropdown value to null. We do that within the <RenderDropdown />:
componentDidMount() {
this.clearInputIfValueDoesntExistInOptions();
}
componentDidUpdate() {
this.clearInputIfValueDoesntExistInOptions();
}
clearInputIfValueDoesntExistInOptions() {
const {
input: { value, onChange },
options
} = this.props;
// Clear input value if it doen't exist in options
if (
value &&
!options.some((option) => option.key === value || option.value === value)
) {
console.log(
"clearInputIfValueDoesntExistInOptions",
"Setting value to null as the selected value does not exist in the current dropdown's options"
);
// If I use input.onChange, it does not update the final form state to null immediately.
onChange(null);
}
}
This does set the value to null, but the form state does not update immediately, so the UI does not update either. It only updates once we change another field value or if we submit the form.
I've made a quick demo that simulates our issue:
https://codesandbox.io/s/react-final-form-inputonchange-null-k7poj
Thanks.