prevent form submission if error with useReducer - javascript

I want to prevent a form submission event ONLY IF the form contains errors, if there isn't any, the event shouldn't be prevented.
This's my current code, it's not working because state still isn't updated when if (state.error === true) check is happening.
I'm also using react-router with its useFetcher() hook. So instead of <form>, I'm using fetcher.Form, but I guess it doesn't matter?
export default function App() {
const [state, dispatch] = useReducer(formReducer, {error: null});
return (
<div className="App">
<form onSubmit={onSubmit}>
<button>btn</button>
</form>
</div>
);
function onSubmit(ev) {
dispatch({type: 'submitted'});
console.log(state.error); // still not updated, logs 'null'
if (state.error === true) {
ev.preventDefault(); // prevent or not prevent depending on dispatch
}
}
function formReducer(state, action) {
switch(action.type) {
case 'submitted': {
let error = true; // here will be a validation function
// if error is going to be true, form submission should be prevented
return {...state, error: error};
}
default:
return state;
}
}
}
Live code: https://codesandbox.io/s/young-fire-72dmxt?file=/src/App.js

Yeah, you cant do it this way. It seems like you are aware that react batches its state updates. What you could do is disable the form by default then only enable it when there are no errors. You might want a validate type thing in your reducer which fires after form fields are updated. As you eluded too, onSubmit will be too late in the process.

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

Alert appearing twice in react

Why this alert box is appearing twice in a simple useReducer Application,
Click here
for the codesandbox link,
To regenerate the scenario
increase the value of input using up arrow (Press only once)
decrease the value of input by down key (Press only once)
here is the code
import "./styles.css";
import { useReducer } from "react";
const reducerFunction = (state, action) => {
switch (action.type) {
case "testing": {
if (action.payload.value > 0) {
return { ...state, testing: action.payload.value };
} else {
alert("some message");
return state;
}
}
default: {
throw new Error("Invalid Action");
}
}
};
export default function App() {
const defaultValue = {
testing: 0
};
const [state, dispatch] = useReducer(reducerFunction, defaultValue);
return (
<input
value={state.testing}
onChange={(e) => {
dispatch({ type: "testing", payload: { value: e.target.value } });
}}
type="number"
/>
);
}
You are using StrictMode which invokes functions passed to useReducer twice, from docs:
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Functions passed to useState, useMemo, or useReducer
// Remove Strict.Mode
ReactDOM.render(
<App />,
rootElement
);
Your initial state is this:
const defaultValue = {
testing: 0
};
Which is bound to the input value, but then from your reducer you're returning:
return { inputValue: action.payload.value };
Causing testing to be undefined.
You can see this error in console:
Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components
Your code should be
return {...state, testing: action.payload.value };
This will show one alert and prevent negative values
Dennis is correct about the strict mode behavior which is causing the issue here.
If you still need to use strict mode in the project you can use the code below to update the same-
Code Sandbox Link

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

React form can be submitted twice despite conditional render

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.

How to track form changes in react for analytics

I am using react, redux and redux-saga. Now the thing is i want to track all the user interactions with the form such as onfocus timestamp, what he enters and when he submits. I want one common component that should do all the things like above for me. How to do this. Thanks in advance
I would disagree that this a perfect use case for a middleware. The tracking is a side-effect of user interaction and not really related to redux, as sending off analytics data does not affect the application state.
You probably want some kind of HOC <TrackableInput> which has event handlers for onChange, onFocus, etc. and each of these handlers fires analytics requests, as a side effect, before dispatching an action like INPUT_CHANGE_VALUE. Separate the concerns of tracking and managing application state.
const trackFormEvent = (type, value = null) => {
console.log('Track form event of type', type, 'with value:', value);
// Do actual tracking request in here.
};
class TrackableInput extends React.Component {
onChange = (changeEvent) => {
const { value } = changeEvent.target;
trackFormEvent('change', value);
this.props.onChange(value);
};
onFocus = (focusEvent) => {
trackFormEvent('focus');
this.props.onFocus(focusEvent);
};
onBlur = (blurEvent) => {
trackFormEvent('blur');
this.props.onBlur(blurEvent);
};
render() {
return (
<input
{...this.props}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
}
}
If you want to track all the action triggered as a result of user actions. Example when user submits the form. You want to log "MY_FORM_ACTION/FORM_SUBMIT" and "MY_FORM_ACTION/FORM_SUBMIT_SUCCESS" or "MY_FORM_ACTION/FORM_SUBMIT_FAILURE" as well. Use middleware.
You can type all your form actions something like "MY_FORM_ACTION/ACTION_NAME"
and write a simple middleware like below
const FormLogger = store => next => action => {
if(action.type.includes('MY_FORM_ACTION/')){
// log your action here
}
next(action)
}
Then in the If condition you can send them to a server or just log it. This middleware will capture all form actions which have "MY_FORM_ACTION/". Add this middleware to your redux just like saga or thunk middleware and it will work!
else You can have a TracMe Component like below.
Const TrackMe = Component => props =>{
const {onChange, onClick, onBlur, ...rest} = props;
const logChange = (e) =>{
//log Change
if(onChange){onChange(e)
}
const logClick = (e) =>{
//log Click
if(onClick){onClick(e)
}
const logBlur = (e) =>{
//log blur
if(onBlur){onBlur(e)
}
return (<Component
onChange={logChange}
onClick={logClick}
onBlur={logBlur}
{...rest}/>)
}
Now if your component is <Input {…props } /> just wrap it with TrackMe(<Input {…props } />

Categories