React useContext losing state - javascript

I have a problem that I've been trying to solve for the past couple of days:
I'm using a Context Provider for form fields and for whatever reason fields keep overwriting each other when I use memo.
Provider:
export const Context = React.createContext();
function Form_Provider({ values, onChange, children }) {
return (
<Context.Provider value={{ values, onChange }}>
{children}
</Context.Provider>
)
});
export default Form_Provider
Field:
function Field({ label, name }) {
const { values, onChange } = useContext(Context);
return (
<Memorable
label={label}
onChange={({ target: t }) => onChange({ name, value: t.value })}
value={values[name]}
/>
);
}
const Memorable = React.memo(props => {
return (
<Form.Item label={props.label}>
<Input
value={props.value}
onChange={props.onChange}
/>
</Form.Item>
</>
)
}, ({ value: newValue}, { value: oldValue }) => newValue == oldValue)
Form
const [formValues, setFormValues] = useState({ field1: 'Foo', field2: 'Bar' });
<Form.Provider
values={formValues}
onChange={({ name, value }) => setFormValues({...formValues, [name]: value }))
>
<Form.Field name='field1' label="Field 1" />
<Form.Field name='field2' label="Field 2" />
</Form.Provider>
(Tried to simplify it as much as possible)
In my actual code I've added a json print prettifier to track the state for every field and it works out until every single field when i only edit one field. However, once I start editing another field the first field I've edited goes back to its original state and/or some other weird in between state from it's past.
If I dont use Memo it works but that can't be the solution as I'll be working with a lot of fields and that would cause a lot of re-rendering.
Anyone any idea what's going on here?
Addition:
I've already tried using an internal reducer for this and passing down a dispatch function. As long as I don't try to manage the state outside of the provider everything works.

I'm pretty sure the issue is that you are memoizing the original values of formValues from here:
onChange={({ name, value }) => setFormValues({...formValues, [name]: value }))
Say field1 has a change in it's value. It calls the onChange handler, which merges ...formValues - the values that existed when the component mounted - with the new value for field1.
Now the equality function in React.memo for field1 returns false, because the value is different. That particular field re-renders to recieve its new value, and also the new values of ...formValues. The other fields, however, have not rerendered. For them, ...formValues still means the value of the state as it existed the last time they re-rendered, which was when the component mounted.
If you now change the value of field2, it will set the state to the result of merging the original state with the new value of field2. Hence field1 is reset because its value has now changed again back to the original value.
A simple solution to this would be to use the callback version of setState, which always uses the state's current value:
onChange={({ name, value }) => setFormValues(fv => {...fv, [name]: value }))
However, I would be tempted not to do this, and instead get rid of the memoisation altogether. This is because your equality function does not actually accurately reflect the way that props provided to the component change. I believe the performance gains here are also negligible, because the component is so small and does not render any additional components itself.
Assuming there's no animation tied to the value change, it is very cheap to perform and does not make a good candidate for memoisation, which also escapes the built in React optimisation. You should think carefully to decide if you really need it before implementing it.

Related

Can the child component receive old data for manipulation from parent component?

Let's define a Tags component (a fancy checkbox group).
const Tags = ({ tags, selectedIds, onSelectionChange }) => {
const createClickHandler = (id) => () => {
const newSelectedIds = xor(selectedIds, [id]);
const selectedTags = newSelectedIds.map((id) =>
tags.find((tag) => tag.id === id)
);
onSelectionChange(selectedTags);
};
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={createClickHandler(id)}
>
{text}
</button>
))}
</div>
);
};
This allows us to consume it like this:
export default function App() {
const tags = someUsers.map((user) => ({
id: user.id,
text: user.name,
value: user
}));
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
return (
<div>
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={setSelectedTags}
/>
</div>
);
}
You can test this in https://codesandbox.io/s/musing-goldwasser-nmm13
I believe this is a decent design of a component and its props (the main focus is on the ease of consuming for the other components). We could perhaps remove selectedIds and add a selected flag in the tags prop, however this is beyond the question scope.
My colleague on the other hand insists that this can lead to bugs and should be avoided.
The reasoning is as follows: if we want to update the state we must use appropriate API - setState(oldState => //data manipulation to produce new state) from useState (https://reactjs.org/docs/hooks-reference.html#functional-updates)
Since the parent passes the state directly to the children we can't be sure that the child component filters data based on the latest data. Basically, this issue: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value
His implementation would be something along these lines:
const Tags = ({ tags, selectedIds, onTagClick }) => {
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => onTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
In this case, we lift the whole filtering to a parent component
const handleTagClick = (id) =>
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
You can test this in: https://codesandbox.io/s/kind-cdn-j7cg3
or another version:
const Tags = ({ tags, selectedIds, setSelectedIds }) => {
const isSelected = (id) => selectedIds.includes(id);
const handleTagClick = (id) =>
setSelectedIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => handleTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
in this case, we leave the filtering to the Tags component however we pass the function which allows modification of state based on old state.
You can test this code in https://codesandbox.io/s/relaxed-leftpad-y13wo
In my opinion, this case is a completely different scenario that React docs never specifically address.
As far as I understand React rendering engine will always ensure that the child nodes get the newest props so a situation where a child component filters (or does other manipulation) with stale data is simply impossible. I would like to quote some docs for this however I haven't found any information on this specific situation.
All I know is:
with my many years of React experience I have yet to encounter any bugs with my approach
other 3rd party libraries use the same design
Can someone (with deep React knowledge) provide more insight why I am correct or wrong in this instance?
For you to notice the difference, you could simulate a delay in the update of the selection. i.e., the user of your component needs to do some async stuff when selecting a tag
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
const asyncSelection = (tags) => {
setTimeout(() => setSelectedTags(tags), 1000);
};
...
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={asyncSelection}
/>
You can try here clicking each option one by one, and when all updates run, not all options will be selected (which is not expected). Since the component didn't render immediately, the handler was not updated and the second click is executed with an old state, therefore, the sequences of the selections are not correctly synced. Of course, this is a contrived example, but it could be the case in a very heavy UI that 2 clicks happen without the Tags component being rerendered.
On the other hand, letting the user have more control over the state would be possible to handle this situation. Once again, if you try here clicking each option one by one, in the end, all will be selected as expected
const handleTagClick = (id) => {
setTimeout(() => {
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
}, 1000);
};
As far as I can see the things you're talking about are two different issues.
If the props of a child are updated it will trigger a rerender of that component. There are edge cases where that gets tricky like with useRef or some callbacks but that's besides the point. The filtering and things you're doing will never be different or affected in any way as long as it's dependent on the props changing and if the component receives new props it will rerender the child and reapply the filters without any issues.
The second issue is sort of different from the first one. What could happen is that the tag state is repeatedly updated and only one of those states are passed to the child, that's what you want to avoid. Essentially you have to make sure the parent state has actually updated correctly before passing it to a child. The child will always update and filter and do everything correctly exactly on what's passed to it, your problem here is making sure you're actually passing the correct props.
There's no need to move anything to the parent component, the child will update itself correctly when the parent tag state updates and passes that new state to the child, the only thing you have to look out for here is that you don't update the parent state multiple times and cause https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value and end up passing the wrong props to the child. For example if someone spams the group checkbox on and off quickly. Even then if you pass the wrong props to the child the child will still update itself and reapply the filtering and everything, just on the wrong props.
React will do its batch state update on something like a 10ms interval (I'm not exactly sure how long it is). So if someone clicks the checkbox and it updates the tag state at 6/10ms it will rerender the component 4ms later when it does the batch state update. If hypothetically during those 4ms you click it off again, or if straight after it updated you click it off again, it's where weird things start happening. This is why if you use the increment counter (like in that example) multiple times it won't actually increase it by 3, only by 1, since the code will execute all 3 times on 0 before it did the state update 10ms later. That being said even if you spam that checkbox on and off all the time (spamming the tag array state), I don't see any way how it would go out of sync, every 10ms it will update and rerender the child and the moment you stop spamming it the child will finally rerender on the last current parent state and be correct. I don't see how you could really have an issue with that in your example. It could cause an issue with something like a counter but not with your tags because of the fact that a counter is a cumulative addition on previous values whereas your tags is a static set of values (that is the key difference).

DOM created dynamically not showing up in Component

I have a function which basically generates dynamic dom as below
const arrMarkup = [];
const getMarkup = () => {
{
if(true){
arrMarkup.push(<Accordion expanded={expanded === cust.name} onChange={handleChange(cust.name)}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography className={salesEvent.name && classes[salesEvent.name]}></Typography>
</AccordionSummary>
</Accordion>
)
})
})
}
return <div>{arrMarkup}</div> ;
}
Now, i am trying to execute this function is on useEffect as below
useEffect(() => {
getMarkup();
}, [cust]);
and trying to add in return of JSX as
return (
<div>
{arrMarkup}
</div>
)
but can not see the markup added, however i can see its added in array arrMarkup. What's wrong?
React only re-renders your component when its state or props change. As far as one can tell from your question, arrMarkup isn't either a state member or a prop. (If it were, directly modifying it would be against the React rules, because you must not directly modify state or props.)
It's hard to tell you what to do without more information, but you probably want arrMarkup to be a state member, for instance (in a functional component using hooks):
const [arrMarkup, setArrMarkup] = useState([]);
and then to update it appropriately, for instance:
setArrMarkup(current => [...current, <NewStuff>goes here</NewStuff>]);
Note that I used the callback version of the state setter. That's important when you're updating state based on existing state, since state updates may be asynchronous and can be batched (so the state information you already have can be stale).
FWIW, a couple of other observations:
It's unusual to have the useEffect dependency be cust (a single object as far as I can tell) and have triggering the effect add an entry to an array that has previous entries for previous values of cust which (apparently) you're no longer storing in the component's state anywhere. That just feels very off, without more context.
You haven't shown the definition of handleChange, but onChange={handleChange(cust.name)} looks like it's probably incorrect. It calls handleChange, passing in cust.name, and the uses its return value os the change handler. Did you mean onChange={() => handleChange(cust.name)}, so that handleChange is called when the event occurs?

React + Formik - how to pass in new values and set form as dirty?

I have a Formik form in my React app and I have a specific scenario I can't seem to find a workaround for:
I have a toggle outside of my custom form component that essentially "sets everything in the form to false". My current solution is when that gets toggled, I update the props I'm passing to form component, and re-rendering:
In my Form component:
const [initialState, setInitialState] = useState(props.initialState);
useEffect(() => {
setInitialState(props.initialState);
}, [props.initialState]);
...
...
<Formik
enableReinitialize
initialState={initialState}
...
/>
This does correctly update my form with the new values, but it doesn't set the form to be dirty. I have logic in my form component based on whether it is dirty, and in this scenario I want the form to be considered dirty. I first tried to set each field to be touched or include each one in initialTouched. However, dirty is computed comparing current values to initialState, not whether fields have been touched, so my form here is considered "pristine" because it is literally the same as my (new) initial state.
What I'm looking for is either:
A way to pass in these new values as something other than initialState (ie somehow imperatively set values from outside <Formik />) that would cause dirty to be computed as true
A way to force my form to be dirty (it's a read-only property, but if there is another way to mimic it and force it to be true)
onSubmit={(values, { setSubmitting, resetForm }) => {
setSubmitting(true);
setTimeout(async () => {
console.log(values);
// it will set formik.isDirty to false
// it will also keep new values
resetForm({ values });
}, 100);
}}
Have you tried using the onReset property? That should allow you to pass values from any source you want.

Adding input box values to existing object based on checkbox

I have a table of data that is rendered via an api call. It displays 4 values from the initial object. I mainly care about the name value as this is what's important later in a post request.
Each row in the table has a checkbox. When the checkbox is selected (true) the name associated with that checkbox is added to an object called selectedFields, as an object. For example If I select the checkbox with name id it creates an object store like:
"selectedFields": {
"id" : {}
}
This works fine and well. However, I've added 3 input boxes that are associated with each name. The inputs are lengthType, size, maxArrayElements, which are of course user selectable.
What I'm having trouble with is adding these values back to the object so it looks like:
"selectedFields": {
"id": {
lengthType: Variable,
size: 1,
maxArrayElements: 1
},
"price": {
lengthType: Fixed,
size: 10,
maxArrayElements: 1
}
}
How can I add these 3 values back to the name object that was created so it looks like the above example?
I don't want to post a wall of code, so I'm posting the checkbox function that handles creating the selectedFields object with the appropriate selected names. I suspect that the input values should get added here somehow, but I'm not sure.
checkbox = ({ name, isChecked }) => {
//handle check box of each fieldName
const obj = this.state.fieldNames.find(field => field.name === name);
if (isChecked === true) {
//checked conditional
this.setState(
{
selectedFields: {
...this.state.selectedFields,
[name]: {
...obj
}
}
},
() => {
console.log(
"callback in isChecked if conditional",
this.state.selectedFields
);
}
);
} else {
const newSelectedFields = this.state.selectedFields;
delete newSelectedFields[name];
this.setState(
{
selectedFields: newSelectedFields
},
() => {
console.log(
`box unchecked, deleted from object --->`,
this.state.selectedFields
);
}
);
}
};
You will have to make the first dropdown selection to view the data.
CodeSandbox link here
Answer
You need to change a few things because nothing is stating where to assign the new state outside of the root state object.
Handlers in your Index.js:
Your handlers for the change events aren't looking for the name of the object to determine if it exists or not. If you want to add it to the specified child object you better be sure it's there.
We adjust the handlers to take an object name and setState with object.assign on that specific object if it exists:
note: since lengthType doesn't have a name property we simply provide it with a string. e.currentTarget will provide the option span, not the root Dropdown element, so even supplying a name property to that component wouldn't allow us to use e.currentTarget.name - you may want to consult the Semantic UI documentation if you would prefer something different. I gave it a quick scan but didn't want to deep dive it.
handleChange = (e, obj_name) => {
if (this.state.selectedFields[obj_name]) {
let selectedFields = Object.assign({}, this.state.selectedFields);
selectedFields[obj_name] = Object.assign(
this.state.selectedFields[obj_name],
{ [e.target.name]: e.target.value }
);
this.setState({ selectedFields });
}
};
onLengthTypeChange = (e, obj_name) => {
if (this.state.selectedFields[obj_name]) {
let selectedFields = Object.assign({}, this.state.selectedFields);
selectedFields[obj_name] = Object.assign(
this.state.selectedFields[obj_name],
{ lengthType: e.currentTarget.textContent }
);
this.setState({ selectedFields });
}
};
The above, of course, won't work if you don't adjust your onChange events on your components so that, in addition to your event object, they also send your object name.
Handlers in your Component file:
Note: It was odd because in your Index.js file you seemed to half do this with lengthType but you weren't passing over additional data. You can't simply pass parameters into a handler - to get it to work you need to pass an anonymous function to the onChange properties that will take the event and pass it on to the handler functions with your object name:
<Table.Cell>
<Dropdown
placeholder="Pick a length Type:"
clearable
selection
search
fluid
noResultsMessage="Please search again"
label="lengthType"
multiple={false}
options={lengthTypeOptions}
header="Choose a Length Type"
onChange={e => onLengthTypeChange(e, name)}
value={lengthType}
required
/>
</Table.Cell>
<Table.Cell>
<Input
onChange={e => handleChange(e, name)}
value={this.state.size}
type="number"
name="size"
min="1"
placeholder="1"
required
/>
</Table.Cell>
<Table.Cell>
<Input
onChange={e => handleChange(e, name)}
value={this.state.maxArrayElements}
type="number"
name="maxArrayElements"
placeholder="1"
min="1"
max="100"
required
/>
</Table.Cell>
Once these things are adjusted, the code will update the specified properties on the child objects after the corresponding checkbox is selected.
Final Note:
I did not adjust it to save the previous state if you uncheck and then check the box. It wasn't specified in your question and I don't want to make assumptions.
Code Sandbox:
The adjusted code sandbox: https://codesandbox.io/s/createqueryschema-table-rewrite-bwvo4?fontsize=14
Additional Recommendations:
In your initial state your selectedFields is declared as an Array and then it is promptly turned into an Object when any checkbox is selected. I would suggest not doing this. Changing data types on a property during the course of running an application is very much asking for trouble.
When a checkbox is loaded you provide a checkbox function from your Index.js file. This is simply called box in your component. I would suggest keeping the names of properties and state equivalent when passing down from parent to child. It is much, much, much easier for someone else to come in and maintain if they have to - not to mention easier to retain your own sanity.
The above checkbox function takes props from child and passes them up to the parent. This would be the place to pass your collected data into a cache on the parent, into local/session storage, or whatever you want to do with your data. You could instead write code to the effect of: if the checkbox is selected when an input handler is called do a save - but I would say that it would probably be best on render since the screen is constantly updating anyway and you have the checkbox function readily passing props currently. This is preference, so it's your call
Good luck! Hope this helped!

React - Best practice with state in form components?

I'm trying to get my head around best practice regarding state in react components. I started creating a form by writing a TextField component as follows
var TextField = React.createClass({
render: function() {
const {value, title, placeholder} = this.props;
return (<div>
{title}
<input type="text"
value={value}
placeholder={placeholder}
onChange={this.handleChange} />
</div>);
},
handleChange (evt){
this.props.onChange(evt.target.value);
}
});
This is a controlled component. So the parent container has to pass a value in for the input in via props and change that value when there is a change. It seems like this is the usual approach.
My problem comes when I want to create a numeric field. For this example assume that my numeric field will allow non numeric characters to be input (the field just won't validate). I don't like the idea of having the validation of that field within the parent so this is what I wrote
var NumericField = React.createClass({
getInitialState: function(){
return{
value : ""
}
},
componentWillReceiveProps: function(nextProps) {
if(this.validate(nextProps.value)){
this.setState({value:nextProps.value});
}
},
validate : function(input) {
return !isNaN(input);
},
render: function() {
const {value} = this.state;
const {title} = this.props;
return (<div>
{title}
<input type="text"
value={value}
onChange={this.handleChange} />
</div>);
},
handleChange (evt){
this.setState({value:evt.target.value});
if(this.validate(evt.target.value)){
this.props.onChange(evt.target.value);
}
}
});
This allows the parent to set the value and update it via the "value" prop, but the "onChange" prop will only trigger when the content is valid. It feels to me like I've used a different pattern for each, and that's not good. Not sure if that's a valid feeling?
I suppose I just wanted to ask if my approach to the numeric field seems reasonable or if there is a better pattern to follow?
Just in case someone wants to know why I want a numeric field to work this way, I don't, it's just a simplified example. A more valid example would be a text area for json, that only called onChange when the content was valid json.
Appreciate any feedback
Setting state by passing in props is generally frowned upon.
Props should be immutable (like your NumericField component's title)
If you want to set an initial value it should come from the controller or store the parent component is getting it from, eg.
getInitialState() {
return({
value: FormDataStore.getInitialNumericFieldValue()
});
}
After that, any changes to the value should be handled by the NumericField component. If you need to validate do so before setting the new state, eg.
handleChange(evt) {
if (this.validate(evt.target.value)){
this.setState({
value: evt.target.value
});
/* You can also pass the new validated value
up to the parent component to hold on to
till you're ready to process the form*/
this.props.onChange(evt.target.value);
}
}
Your state will now only ever hold (and subsequently the parent component will only ever receive) the last validated value so you could even display valid/invalid message if this.state.value === input, but that's extra
Incidentally, your TextField component should also follow this pattern. Passing a changed value up to the parent just to have it passed down again as a prop defeats the purpose of having a child component. In that case I would have the JSX (and any validation process) all in the parent component instead of abstracting a child. Unless the child component could be re-used. But then I'd still let the child handle its own state.

Categories

Resources