React form can be submitted twice despite conditional render - javascript

I have a React application coupled to Redux. There is a component rendering a form wrapper (a custom implementation of Formik), while the form inputs themselves are rendered by a child component.
(Not the exact code, but gets the point across.)
...
render() {
const {
config,
updateContactDetails,
errorMessages,
contactDetails,
previousFormValues,
isUpdating,
} = this.props;
const { apiBaseUrl, fetchTimeout, globalId } = config;
const initialValues = previousFormValues || getInitialContactDetailsValues(contactDetails);
if (isUpdating) return <Spinner />;
return (
<Form
initialValues={initialValues}
validate={(values) => validate(values, errorMessages)}
onSubmit={(values) => {
updateContactDetails(apiBaseUrl, globalId, values, fetchTimeout); // dispatch action
}}
>
<ContactDetailsForm content={content} />
</Form>
);
}
...
When you click the submit button in ContactDetailsForm, the value of isUpdating in the Redux store is set to true. As you can see above, that causes the the form to be replaced with a spinner component. However, it is somehow possible to submit the form twice by clicking the button twice.
How can this be? Could there be re-render happening before the one that replaces the form with the spinner? I know I can solve the problem by passing isUpdating into ContactDetailsForm and using it to disable the button, but I still want to illuminate the cause.
EDIT
The reducer looks something like this, in case it helps:
case UPDATE_CONTACT_DETAILS_START: {
return {
...state,
errorUpdatingContactMethods: {},
hasUpdatedContactDetails: false,
isUpdating: true,
contactDetailsValues: action.values,
};
}

You should instead set a disabled property on the button based on the isUpdating prop. It might be that it's just a race condition.

Related

React/Redux component with checkboxes does not update when click on checkbox even though I'm returning new state

I've been stuck for a while trying to make the re-render in the checkboxes to work, the variables are being updated but it's just the rendering that doesn't happen.
I'm receiving a response from the backend that contains an object with an array of steps, I'm going to render a checkbox for every step if it's from a specific type. As soon as I received the object, I add in every step a new property value to use it later for checking the checkboxes.
This is my reducer:
export const MyObject = (state: MyObject = defaultState, action: FetchMyObjectAction | UpdateStepsInMyObjectAction) => {
switch (action.type) {
case "FETCH_MYOBJECT":
return {
...action.payload, // MyObject
steps: action.payload.steps.map((step) => {
if (step.control.controlType === "1") { // "1" = checkbox
return {
...step,
value: step.control.defaultValues[0] === "true" ? true : false, // Adding the property value
};
}
return step;
}),
};
case "UPDATE_STEPS":
return {
...state,
steps: state.steps.map((step) => {
if (step.id === action.payload.stepId) { // if this is the checkbox to update
return {
...step,
value: action.payload.checked,
};
}
return step;
}),
};
default:
return state;
}
This is how I'm rendering the checkboxes:
for (let step of steps) {
if (step.control.controlType === "1") {
controls.push(
<Checkbox
label={step.displayName}
checked={step.value}
onChange={(_ev, checked) => {
callback(step.id, checked);
}}
disabled={false}
className={classNames.checkbox}
/>
);
}
}
callback is a function that calls the reducer above for the case "UPDATE_STEPS".
After inspecting the variables I can see that they are being updated properly, it's just that the re-render doesn't happen in the checkboxes, not even the first time I check the box, the check doesn't appear. If I move to a different component and then go back to the component with the checkboxes I can see now the checks. But if I check/uncheck within the same component, nothing happens visually.
As far as I know, I'm returning new objects for every update, so mutability is not happening. Can you see what I'm missing?
Thanks!
First I would inspect if the checkbox works with useState to manage your state.
import { useState } from "react";
function CheckBoxForm() {
const [checked, setChecked] = useState(false);
return <Checkbox checked={checked} onChange={() => setChecked(!checked)} />;
}
Then I would check if you have wired up the reducer correctly using redux or useReducer. When you dispatch an action it should trigger a rerender. For troubleshooting this using redux please refer to the redux docs: https://react-redux.js.org/troubleshooting#my-views-aren-t-updating-when-something-changes-outside-of-redux.
You may be updating the object directly rather than dispatching an action using the function provided by the redux store. If you are using hooks, here is how you wire up your app to make sure the component props are subscribed to changing in the redux store. You must wrap with a provider, use redux hooks like useSelector and use their provided dispatch function: https://react-redux.js.org/api/hooks
Using useReducer is a much simpler process and will be easier to troubleshoot: https://beta.reactjs.org/reference/react/useReducer

React/Redux Toolkit render issue when deleting state from an array

Problem: Component render starts to drift from actual state
Desired Output: Component render matches state.
So. I'm going to give a bit of a high-level overview with pseudocode as this issue is quite complex, and then I'll show the code.
I have a main form, and this form has an array of filter-states that are renderable in their own components. These filter-states are a one-to-many relationship with the form. The form has-many filter-states.
form: {
filters: [
filter1,
filter2
]
}
Say you want to remove an item from the state, you would do something like so in the reducer (redux)
state.form.filters.filter(f => f.id != action.payload.id)
All good. The state is updated.
Say, you want to render this state, you would do something like so:
// component code ommited, but say you get your form state from redux into the component
formState.filters.map(filter => <FilterComponent filter={filter}/>
All good. your filters are being injected into the component and everyone is happy
Now. This is where it gets weird pretty quickly.
There is a button on my FilterComponent, that says delete. This delete button goes to the reducer, runs the code to delete the filter from the formstate (as you saw above), and yes, it DOES work. The state gets updated, BUT, the UI (the array of components) starts to drift from the state. The UI shows previously deleted states, and states that should be persisted are not shown (but in the redux tab on chrome, the state is CORRECT...!)
The UI acts as if the array of states is being pop()'d; no matter how you remove the states, it will remove the final state in component render.
Now, for the code.
// This takes a list of filters from the form state and loads them into individual form components
const Filters: NextPage<Filters> = () => {
const formState = useSelector((state: any) => state.form.formState)
// In the hope that state change will force reload components, but no avail
useEffect(() => {
console.log("something has been reloaded")
}, [formState])
return (
<>
{formState.form.map((filter, i) => {
return <FilterForm defaultState={filter} key={i} index={i} />
})}
</>
);
};
export default Filters;
The form for these individual states:
Please note, this is obviously redacted a lot but the integral logic is included
const FilterForm: NextPage<FilterFormProps> = ({ defaultState, index }) => {
const formState = useSelector((state: any) => state.form.formState)
// Local component state; there are multiple forms so the state should be localised
const [FilterState, setFilterState] = useState(defaultState)
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const updateParentState = async () => {
dispatch(updateForm(filterState))
}
useEffect(() => {
updateParentState()
}, [filterState])
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} name={filterState.id} />
<Input
name="filter_value"
onChange={handleOnChange} // does standard jazz
value={filterState.filter_value} // standard jazz again
/>
)
}
Now what happens is this: if I click delete, redux updates the correct state, but the components display the deleted state input. Ie, take the following:
filter1: {filter_value: "one"}
filter2: {filter_value: "two"}
filter3: {filter_value: "three"}
these filters are rendered in their own forms.
Say, I click delete on filter1.
filter1 will be deleted from redux, but the UI will show two forms: one for filter1 and one for filter2.
This drift from UI to state baffles me. Obviously I am doing something wrong, can someone spot what it is?!
So, I fixed the issue.
As it turns out, there isnt really an explanation for why the above behaved as it does, but it does warrant for a better implementation.
The issue was as follows; the redux state was conflicting with the local state of the rendered components it was injected in. Why it did, is another story. Somehow, while injecting the redux state into the component and assigning it to the local state, the states went a bit haywire and drifted apart.
The solution was to get rid of the local state (filterState), the updateParentState function call and rather to update the localised state directly through the parent state that it resides in.
The new component looked something like the following:
const FilterForm: NextPage<FilterFormProps> = ({ state, index }) => {
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const handleChange = (e) => {
dispatch(updateFormFilterState({ ...state, [e.target.name]: e.target.value }))
}
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} />
<Input
name="filter_value"
onChange={handleChange}
value={state.filter_value}
/>
)
}
Hope this answer helps someone with the same issue as me.

Submit form on prop value changing

I am new to React and trying to modify this application: https://github.com/nice-table/bitmex-scaled-orders
My goal is, say the prop "instrumentData" found in "src/modules/orders/OrderForm.js" has "instrumentData.lastprice" value changing to a specific value in real-time in the backend. I want to submit the form on that page if the value reaches a specific value. In other words, I want to keep monitoring that prop untill it hits a number and it will submit the form upon that. Is that doable through states? I tried to research it but given I am new to React I am a bit lost as to what code to use and where exactly to add it.
Thanks.
Autosubmitting is simple
It's simple to run some action on data change. React components are data driven - autoupdating. You can just insert a function 'into data flow'.
Your data source is in DataContext then you should use <DataContext.Consumer /> to get data 'stream' - stream because it's frequently updated using socket connection.
<DataContext.Consumer>
{ (data, submitForm, isSubmitting) => {
console.log("context data", data );
// extract data from `data` object
// const someData = data.someProperty;
// if( someData > 12345 ) {
// if( !isSubmitting ) {
// submitForm()
// }
// return "Limit reached"
// }
// return null
}}
</ DataContext.Consumer>
This snippet can be placed almost anywhere after this code:
render={({
values,
errors,
touched,
setFieldValue,
submitForm,
isSubmitting,
isValid,
validateForm
}) => (
<React.Fragment>
// place it here
... and of course before end of this fragment (</ React.Fragment>).
You can pass and use almost all functions defined in this component (file), f.e. setFieldValue("priceUpper", to update form value before submitting.
Autosubmitting is NOT so simple
Problem is not trivial. You should create a component with internal logic to:
set limit (render input, onChange handler, useState) instead of hardcoded value
block(or not? checkbox?) autosubmitting in a loop (formik will submit but later it will clear isSubmitting flag - our component will autosubmit again)
render context consumer inside - optimize rerenderings
etc.
Good luck ;)
React has a really good write up on forms and handling onChange() which is an event that can fire when a field is changed https://reactjs.org/docs/forms.html.
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
}
submit = () => {
// submit form
// eg axios.post(URL, {
// value: this.state.value
// })
}
handleChange(event) {
this.setState({value: event.target.value});
if (this.state.value == 10) {
this.submit()
}
}
return (
<form>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
</form>
);
}
}
I see the github repo is using hooks in lieu of component classes.
You can read about hooks here https://reactjs.org/docs/hooks-intro.html.
onChange can still call handle change and instead of this.state you may be using useState

How to redirect/collapse an expanded edit form after hitting submit in React-Admin

I am currently trying to collapse the edit form from the list view in React-Admin after hitting the submit button. I want to be able to view those updated changed in the new list form after hitting submit as well.
Right now I have a class which handles when the form is submitted, and I am using window.location.replace('URL') within that class to try and redirect the page. The problem I am currently running into is that since I want the page to redirect to the same URL, It doesn't show the changes made and also doesn't collapse the edit form. When I use a different URL and go back to the list view it shows all the changes. So what I essentially want is to just reload the page (so everything is updated and the edit bar is collapsed) after the form is submitted. However, when I used window.location.reload(), it reloads the page immediately before saving any of the data from the edit form.
This is the code I currently have which handles submit:
const saveWithNote = (values, basePath, redirectTo) =>
crudCreate(
'usergrouprights',
{
...values,
GroupRights: CalcGroupPermissions(values).rights,
GroupDenials: CalcGroupPermissions(values).denials
},
basePath,
{
redirectTo
}
);
class SaveWithNoteButtonView extends Component {
handleClick = () => {
const { basePath, handleSubmit, redirect, saveWithNote } = this.props;
return handleSubmit(values => {
saveWithNote(values, basePath, redirect);
window.location.replace('/#/usergrouprights');
// window.location.reload();
});
};
render() {
const { handleSubmitWithRedirect, saveWithNote, ...props } = this.props;
return (
<SaveButton handleSubmitWithRedirect={this.handleClick} {...props} />
);
}
}
const SaveWithNoteButton = connect(
undefined,
{ saveWithNote }
)(SaveWithNoteButtonView);
export default SaveWithNoteButton;
In the list view, I have a custom toolbar which is the following:
const PostEditToolbar = props => (
<Toolbar {...props}>
<SaveWithRightsValuesButton />
</Toolbar>
);
When the code is run, It submits everything properly but I need to manually reload the page to view the changes and for the edit form to collapse as well. I am unsure if using the window.location.replace or reload call is the right idea as it is not working the way I would like it to at the moment.
Try adding
redirect="list"
to SaveButton:
<SaveButton
handleSubmitWithRedirect={this.handleClick}
redirect="list"
{...props}
/>

ReactJS/GraphQL: Call query from submit button

When my app loads the query below runs and the result from the DB is displayed in the browser. However, my app also has a submit button. How can run this entire component when the submit button is pressed while passing the timestamp input from the submit?
handleSubmit(event) {
event.preventDefault();
console.log(this.state.inputValue)
this.state = {
inputValue: new Date(document.getElementById("time").value).valueOf()
};
console.log(this.state);
}
This is the UserList component code:
const UserList = props => (
<Query
query={gql`
query action($timestamp: Float!) {
action(timestamp: $timestamp) {
action
timestamp
object {
filename
}
}
}
`}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<Item.Group divided>
{data.action.map(action => (
<div>
<ul>
<li>{action.action}</li>
<li>{action.timestamp}</li>
<ul>
{action.object.map(obj => {
return <li>{obj.filename}</li>;
})}
</ul>
</ul>
</div>
))}
</Item.Group>
);
}}
</Query>
);
export default UserList;
Parent component contains submit button and there lives state. Submit handler should only set value in state with ... setState() - NOT directly!
If there is no default state/value use conditional rendering ... in parent render display some placeholder when state (timestamp) is undefined. If value exists pass it as prop:
{!this.state.inputValue ? <SomePlaceholder />
: <UserList timestamp={this.state.inputValue} />}
// place input, submit button where you want (after/before results)
UserList will be called with props - 'props.timestamp' can be used in graphql query literal.
When input changes again submit handler will change parent state and that change will be passed again as prop to child, query fired, results rerendered.
Base on the information provided, there's probably several ways to accomplish what you need. I would say the UserList component needs to conditionally render the Query component in some manner. Perhaps UserList takes a boolean prop isSubmitted and while isSubmitted is false, UserList renders a div with text explaining to submit the query. This could be done as the UserList and the submit button being hosted by the same parent component that has state containing a isSubmitted property that changes on click.

Categories