getting setstate to cause a rerender every time in a useEffect block - reactjs

I've created this codesandbox example and here is the code:
import React, { ReactNode, useState } from "react";
import { Formik, FormikConfig, FormikProps, Form, FormikErrors } from "formik";
import { useEffect } from "react";
import { scrollToValidationError } from "./scrollToValidationError";
// const isEmpty = (a: unknown): boolean =>
// typeof a === "object" && Object.keys(a).length > 0;
export type FormContainerProps<V> = {
render({
values,
errors,
invalid,
submitCount,
isSubmitting
}: {
values: V;
invalid: boolean;
errors: FormikErrors<V>;
submitCount: number;
isSubmitting: boolean;
}): ReactNode;
additionalContent?: ReactNode;
nextButtonText?: string;
} & Pick<FormikConfig<V>, "initialValues" | "validate"> &
Partial<Pick<FormikConfig<V>, "onSubmit">>;
export const FormContainer = function FormContainer<V>({
initialValues,
additionalContent,
validate,
render,
...rest
}: FormContainerProps<V>) {
const [hasValidationError, setHasValidationError] = useState(false);
useEffect(() => {
if (!hasValidationError) {
return;
}
scrollToValidationError();
}, [hasValidationError]);
return (
<>
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={async (values, { validateForm }) => {}}
>
{({
isSubmitting,
submitCount,
isValid,
errors,
values
}: FormikProps<V>) => {
const invalid = !isValid;
if (submitCount > 0 && invalid) {
setHasValidationError(true);
}
return (
<>
<div data-selector="validation-summary">Validation Summary</div>
<Form>
<div>
<div>
{render({
values,
errors,
isSubmitting,
invalid,
submitCount
})}
</div>
<div>
<button type="submit">SUBMIT</button>
</div>
</div>
</Form>
</>
);
}}
</Formik>
</>
);
};
Basically I am calling setHasValidationError(true) which breaks the dependency watcher on useEffect
useEffect(() => {
if (!hasValidationError) {
return;
}
scrollToValidationError();
setTimeout(() => setHasValidationError(false));
}, [hasValidationError]);
But if this is a form with multiple errors then I want to trigger the useEffect every time but I don't know when to reset it to false or if there is a better way.

In order to scroll to the first error field upon clicking on submit then you can do the following:
Write a custom component (eg: FocuseabelField) that renders formik field which also handles automatic scroll to element and focus on error input
Use Formik's innerRef
Just use the formik's isSubmitting and errors to handle logic for scrolling and focussing
FocuseabelField custom component
const FocuseabelField: any = props => {
const elementRef = useRef<HTMLDivElement>();
if (
props.isSubmitting &&
elementRef.current !== undefined &&
props.errors.hasOwnProperty(props.name)
) {
elementRef.current.scrollIntoView();
elementRef.current.focus();
}
return <Field {...props} innerRef={elementRef} />;
};
Usage
<FocuseabelField
errors={errors}
isSubmitting={isSubmitting}
name="name"
placeholder="enter name"
className={errors && errors.name ? "input error" : "input"}
/>
I have taken your code and have commented the stuff like scrollToValidationerror.ts, dom.ts, wait.ts, useState(hasValidationError), useEffect etc.
Simplified working copy of the code is here. I have used 2 fields to demonstrate multiple errors & auto scroll & focus to the error field:
https://codesandbox.io/s/usemachine-typescript-problems-tns0c?file=/src/components/Home/index.tsx
When the forms gets bigger it becomes complicated to manage so its good to consider outsourcing the form validation part and use libraries such as yup and maintain a validation schema and pass it on to formik.
Have a look at the formik docs for examples.

What about creating an Object with keys for each form field? That way you can maintain a specific form validation error for each input and use that Object in the useEffect second parameter, it will make sure it's triggered for each form error update

To answer your original question,
useEffect does reference check for its dependencies, so you could use Object instead of value. So something like this.
const [hasValidationError, setHasValidationError] = useState({value: false});
useEffect(() => {
if (!hasValidationError.value) {
return;
}
scrollToValidationError();
}, [hasValidationError]);
setHasValidationError({value: true});
But in regards to Formik usage, I highly recommend you follow what #gdh pointed out.

I don't understand the need of effects here.
Why don't you call the method directly instead of using hooks?
You can avoid 2 re-renders by doing that, and the component can also be stateless!
...
}: FormikProps<V>) => {
const invalid = !isValid;
if (submitCount > 0 && invalid) {
scrollToValidationError();
}
...

Related

Chakra UI - Checkbox Group

I am using Chakra UI with React Typescript and implementing a checkbox group
The default values are controlled by an outside state that is passed down as a prop.
The problem is that the CheckboxGroup doesn't accept the default values from outside source
The code is as follows:
import React, {FC, useCallback, useEffect, useState} from "react";
import { CheckboxGroup, Checkbox, VStack } from "#chakra-ui/react";
interface IGroupCheckbox {
values: StringOrNumber[],
labels: StringOrNumber[],
activeValues: StringOrNumber[]
onChange: (value:StringOrNumber[])=> void
}
const GroupCheckbox:FC<IGroupCheckbox> = ({
values,
labels,
activeValues,
onChange
}) => {
const [currActiveValues, setCurrActiveValues] = useState<StringOrNumber[]>();
const handleChange = useCallback((value:StringOrNumber[]) => {
if(value?.length === 0) {
alert('you must have at least one supported language');
return;
}
onChange(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(()=>{
if(activeValues) {
setCurrActiveValues(['en'])
}
},[activeValues])
return (
<CheckboxGroup
onChange={handleChange}
defaultValue={currActiveValues}
>
<VStack>
{values && labels && values.map((item:StringOrNumber, index:number)=>
{
return (
<Checkbox
key={item}
value={item}
>
{labels[index]}
</Checkbox>
)
}
)}
</VStack>
</CheckboxGroup>
)
}
export default GroupCheckbox
When I change the defaultValue parameter, instead of the state managed, to be defaultValue={['en']} it works fine, but any other input for this prop doesn't work.
I checked and triple checked that the values are correct.
Generally, passing a defaultValue prop "from an outside source" actually does work. I guess that in your code, only ['en'] works correctly because you explicitly use setCurrActiveValues(['en']) instead of setCurrActiveValues(activeValues).
The defaultValue prop will only be considered on the initial render of the component; changing the defaultValue prop afterwards will be ignored. Solution: make it a controlled component, by using the value prop instead of defaultValue.
Note that unless you pass a parameter to useState(), you will default the state variable to undefined on that initial render.
Side note: You also don't need a separate state variable currActiveValues. Instead, you can simply use activeValues directly.
const GroupCheckbox = ({ values, labels, activeValues, onChange }: IGroupCheckbox) => {
const handleChange = useCallback(
(value: StringOrNumber[]) => {
if (value?.length === 0) {
alert("you must have at least one supported language")
return
}
onChange(value)
},
[onChange]
)
return (
<CheckboxGroup onChange={handleChange} value={activeValues}>
<VStack>
{labels && values?.map((item: StringOrNumber, index: number) => (
<Checkbox key={item} value={item}>
{labels[index]}
</Checkbox>
))}
</VStack>
</CheckboxGroup>
)
}

React ref called multiple times

Having such simple React (with the react-hook-form library) form
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, formState: { errors } } = useForm();
const firstNameRef1 = useRef(null);
const onSubmit = data => {
console.log("...onSubmit")
};
const { ref, ...rest } = register('firstName', {
required: " is required!",
minLength: {
value: 5,
message: "min length is <5"
}
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<hr/>
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e)
firstNameRef1.current = e // you can still assign to ref
}} />
{errors.firstName && <span>This field is required!</span>}
<button>Submit</button>
</form>
);
}
I'm getting:
...ref
...ref
...onSubmit
...ref
...ref
in the console output after the Submit button click.
My question is why is there so many ...ref re-calls? Should't it just be the one and only ref call at all?
P.S.
When I've removed the formState: { errors } from the useForm destructuring line above the problem disappears - I'm getting only one ...ref in the console output as expected.
It is happening because you are calling ref(e) in your input tag. That's why it is calling it repeatedly. Try removing if from the function and then try again.
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e) // <-- ****Remove this line of code*****
firstNameRef1.current = e // you can still assign to ref
}} />
My question is why is there so many ...ref re-calls?
This is happening because every time you render, you are creating a new function and passing it into the ref. It may have the same text as the previous function, but it's a new function. React sees this, assumes something has changed, and so calls your new function with the element.
Most of the time, getting ref callbacks on every render makes little difference. Ref callbacks tend to be pretty light-weight, just assigning to a variable. So unless it's causing you problems, i'd just leave it. But if you do need to reduce the callbacks, you can use a memoized function:
const example = useCallback((e) => {
console.log("...ref")
ref(e);
firstNameRef1.current = e
}, [])
// ...
<input {...rest} name="firstName" ref={example} />

Adding validation to simple React + Typescript application

I have kind of an easy problem, but I'm stuck because I do not know TypeScript well (and I need to get to know it very quickly).
I have to add simple validation on submit which will check if a value is not empty.
I have the simplest React form:
type FormValues = {
title: string;
};
function App() {
const [values, handleChange] = useFormState<FormValues>({
title: ""
});
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
console.log("SUBMITTED");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
/>
<button type="submit">submit</button>
</form>
</div>
);
}
Custom hook to handle form:
import { useCallback, useReducer } from "react";
type InputChangeEvent = {
target: {
name: string;
value: string;
};
};
function useFormState<T extends Record<string, string>>(initialValue: T) {
const [state, dispatch] = useReducer(
(prevState: T, { name, value }: InputChangeEvent["target"]) => ({
...prevState,
[name]: value
}),
initialValue
);
const handleDispatch = useCallback(
(evt: InputChangeEvent) => {
dispatch({
name: evt.target.name,
value: evt.target.value
});
},
[dispatch]
);
return [state, handleDispatch] as const;
}
export default useFormState;
Now I'd like to add simple validation on submit and (because of TS) I have no idea how. I've thought about three options:
Put the validation logic to the handleSubmit method.
Put the validation logic inside custom useFormState hook.
Create another custom hook just to manage validation only.
I tried to handle the first (and I think the worst) solution in this CodeSandbox example, but as I said the TS types are stronger than me and the code does not work.
Would anyone be so kind and help me with both cases (pick the most correct solution and then, run the code with TS)?
A common approach would be to just store the form error value in a React useState variable and render it as necessary.
const [error, setError] = React.useState('');
// ...
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
// This might be the wrong way to check the input value
// I haven't worked directly with form submit events in a hot minute
if (!evt.target.value) {
setError('Title is required');
}
};
// ...
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
aria-describedby="title-error"
required
aria-invalid={!!error}
/>
{error && <strong id="title-error" role="alert">{error}</strong>}
Notice that the aria-describedby, required, aria-invalid and role attributes are important to enforce semantic relationships between the input and its error, announce the error to screen readers when it appears, and designate the input as "required".
If you had multiple inputs in the form, you can make your error value into an object that can store a separate error for each field of your form:
const [errors, setErrors] = React.useState({});
// ...
setErrors(oldErrors => ({...oldErrors, title: "Title is required"}));
// ...
{errors.title && <strong id="title-error" role="alert">{errors.title}</strong>}
Another common pattern is to clear an input error when it is modified or "touched" to allow the form to be resubmitted:
onChange={(e) => {
setError(''); // or setErrors(({title: _, ...restErrors}) => restErrors);
handleChange(e);
}}
Note that all of this error handling logic can be rolled into your custom hooks for form/input handling in general, but does not have to be.

Async update of Formik initialValues inherited from parent React component state (leveraging useEffect hook?)

I am currently building a multi-step form during a user onboarding process, which is why I need to centralize all form data in a parent React component state.
I need to update initialValues with user information but this is an async process.
I thought of creating a useEffect hook calling setState, but maybe there is a more elegant way of doing so...
Having initialValues as one of useEffect dependencies seems to create an infinite loop (Maximum update depth exceeded). This is why the working solution I found was to duplicate all initialValues within... 😒
So how could I update only specific values from initialValues after getting async user information?
Here is a simplified version of the implementation:
import React, { useState, useEffect } from 'react'
// Auth0 hook for authentication (via React Context).
import { useAuth0 } from '../../contexts/auth/auth'
import { Formik, Form, Field } from 'formik'
export default () => {
const { user } = useAuth0()
const initialValues = {
profile: {
name: '',
address: '',
// Other properties...
},
personalInfo: {
gender: '',
birthday: '',
// Other properties...
},
}
const [formData, setFormData] = useState(initialValues)
const [step, setStep] = useState(1)
const nextStep = () => setStep((prev) => prev + 1)
useEffect(() => {
const updateInitialValues = (user) => {
if (user) {
const { name = '', gender = '' } = user
const updatedInitialValues = {
profile: {
name: name,
// All other properties duplicated?
},
personalInfo: {
gender: gender,
// All other properties duplicated?
},
}
setFormData(updatedInitialValues)
}
}
updateInitialValues(user)
}, [user, setFormData])
switch (step) {
case 1:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="profile.name" type="text" />
<Field name="profile.address" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
case 2:
return (
<Formik
enableReinitialize={true}
initialValues={formData}
onSubmit={(values) => {
setFormData(values)
nextStep()
}}
>
<Form>
<Field name="personalInfo.gender" type="text" />
<Field name="personalInfo.birthday" type="text" />
{/* Other fields */}
<button type="submit">Submit</button>
</Form>
</Formik>
)
// Other cases...
default:
return <div>...</div>
}
}
it's probably late for me to see this question and I just happen to work on a similar project recently.
For my use case, I'm using only one Formik, and using theory similar to Formik Multistep form Wizard: https://github.com/formium/formik/blob/master/examples/MultistepWizard.js for my multistep forms.
And on each step, I need to fetch API to prefill data, I also use useEffect but since I just call the API onetime when I load the specific step, I force it to behave the same as ComponentDidMount(), which is to leave the [] empty with the comment // eslint-disable-next-line so it won't give me warning.
And I use setFieldValue in the useEffect after data is successfully loaded. I feel mine is also not a good way to handle this situation, and I just found something that might be useful: https://github.com/talor-hammond/formik-react-hooks-multi-step-form, it has a Dynamic initialValues. (Though it's typescript)
I am going to refer to this and also try to use for each of my steps, and probably use Context or Wrap them in a parent and store data in the parent Formik.
And getting infinite loop for you might because setFormData should not be in the dependency, since when you setState, the component re-render, the useEffect calls again.
Not sure if this can help you or you already find out how to implement it, I'll look into this deeper.

Handling custom component checkboxes with Field as in Formik

I've been stuck for past couple of hours trying to figure out how to handle a form with a custom checkbox component in formik. I'm passing it via the <Formik as >introduced in Formik 2.0
Issue arises with input type='checkbox' as I can't just directly pass true or false values to it.
Now I'm posting the solution which I am aware is a bad implementation.
I didn't really find a way to properly pass values from the component,
so I wanted to hande it as a separate state in the component as the
checkbox will take care of its own state.
My custom input component is structured the following way
import React, { useState } from 'react';
import { StyledSwitch, Wrapper } from './Switch.styled';
type Props = {
value: boolean;
displayOptions?: boolean;
optionTrue?: string;
optionFalse?: string;
};
const Switch: React.FC<Props> = (props: Props) => {
const { value, optionTrue = 'on', optionFalse = 'off', displayOptions = false } = props;
const [switchVal, setSwitchVal] = useState<boolean>(value);
const handleSwitchChange = (): void => setSwitchVal(!switchVal);
return (
<Wrapper styledVal={switchVal}>
<StyledSwitch type="checkbox" checked={switchVal} onChange={handleSwitchChange} />
{displayOptions && (switchVal ? optionTrue : optionFalse)}
</Wrapper>
);
};
export default Switch;
The ./Switch.styled utilizes styled-components but they are not relevant to this question. Imagine them simply as an <input> and <div> respectively
Now here's the component which handles the switch
import React, { useState } from 'react';
import { Formik, Form, Field } from 'formik';
import Switch from '../../../components/forms/Switch';
const QuizMenu: React.FC = () => {
const [isMultipleChoice, setIsMultipleChoice] = useState<boolean>(false);
const sleep = (ms: number): Promise<number> => new Promise((resolve) => setTimeout(resolve, ms));
return (
<Formik
initialValues={{ isMultipleChoice: 'false', password: '' }}
onSubmit={async (values): Promise<boolean> => {
await sleep(1000);
JSON.stringify(values, null, 2);
return true;
}}
>
{
(): any => ( // to be replaced with formik destruct, but dont want eslint problems before implementation
<Form>
<div>
<Field as={Switch} onClick={setIsMultipleChoice(!isMultipleChoice)} value={isMultipleChoice === true} name="isMultipleChoice" displayOptions />
{ isMultipleChoice }
</div>
</Form>
)
}
</Formik>
);
};
export default QuizMenu
;
Which yields the following error:
Error: Maximum update depth exceeded. This can happen when a component
repeatedly calls setState inside componentWillUpdate or
componentDidUpdate. React limits the number of nested updates to
prevent infinite loops.
I also tried editing the value to string as per input type='checkboxed'but I can't really find a way to handle it. If you handle it in a separate handleChange() function you get rid of the error, but then the state doesn't update for some reason.
What would be the proper way of handling this?

Resources