Open zlanich opened 5 years ago
Thanks for the detailed write up and use case. Very very helpful. I think it is a good one. FWIW The way we write our REST API's @palmerhq influenced the design of Formik. We often submit the entire settings object on save instead of each field like you described. It's worth considering discussing or explaining that in more depth in a blog post some day. Anyways....
First, I have been making some relevant progress in this PR: #1578. This changes the way that low-pri validation works so as to only validate one field at a time like you described as your desired behavior. Submission and pre-submit validation is still of the entire form. However, you could diff initialValues and values in you onSubmit handler and then you'd know what changed.
Second, I'm not fully certain that your exact use case is a good one for Formik. because you really are not doing anything at the form level at all. If your validation AND submission are on a per-field basis, then there isn't a need for formik's submission process or colocation. Validate on your own and then just call your real submission function directly in your own component. Somethign like this:
const FormContext = React.createContext({})
const FormLib = ({children, onSave }) => {
const ctx = { onSave: onSave, initialValues }
return (
<FormContext.Provider value={ctx}>
{children}
</FormContext.Provider>
)
}
const useTextField = ({ name, validate }) => {
const form = React.useContext(FormContext)
const [value, setValue] = React.useState(form.initialValues[name])
const [error, setError] = React.useState(undefined)
// if you change initialValues, then reset local state
React.useEffect(() => {
setValue(form.initialValues[name])
setError(undefined)
}, [form.initialValues, name])
const debouncedSave = debounce(form.onSave, 300)
const onChangeText = (text) => {
setValue(text)
if (validate) {
const maybeError = validate(text)
setError(maybeError)
if (!maybeError) {
debouncedSave(text)
.then(
() => console.log('saved at: ' + Date.now()),
(err) => console.log(err)
)
}
}
}
return [{ value, onChange }, { error }
}
const TextField = (props) => {
const [field, { error }] = useTextField(props)
return (
<View>
<TextInput {...field} {...props} />
{!!error ? <Text>{error}</Text> : null}
</View>
)
}
const Settings = ({ settingsValues }) => {
return (
<FormLib
initialValues={settingsValues}
onSave={value => SaveMyField(value).then(() => console.log('boop')}
>
<TextField name="firstName" validate={value => value.trim() != null} />
<TextField name="lastName" />
</FormLib>
)
}
Third, with Hooks / v2, there is no safe way to emit an effect after a commit. This API is not feasible because useState
's setter does not give a callback fn and there isn't a way to emit an effect after useReducer
has committed the update. My guess is that Fiber/ConcurrentMode makes this kind of API even harder to predict/reason about and the general suggestion is to lift this logic into useEffect
. In fact, returning a promise or callback from Formik's setters / handlers would actually be super misleading at the moment in v2 because the promise resolution would not be actually related to the commit of the state update in React. HOWEVER, Sophie did write a useReducerWithEmitEffect
hook in a gist that supposedly enables this exact behavior. I'm a little hesitant though to jump on something that is not yet officially supported (even if its from Sophie).
Fourth, diff tracking or submitting changed values has always been something that I expected folks to do on their own inside of onSubmit because that's likely how you would do it on your own in React. Even if we merge #1578, I don't think i would change this.
Thanks @jaredpalmer! This is all very interesting, and you've brought some things to my attention that I wasn't aware of regarding useEffect() and the misleading promise resolution issue. The example you gave has my gears turning now on an overall better future solution.
I've been on the fence about whether Formik is the best solution for my use case, and here's why:
In settings screens, you have a mixture of independent and dependent fields. Some are best submitted together (like a form), while some are inherently independent. Currently, I'm wrapping these cases in their own separate <SettingsForm>
components. Example:
So it pained me to completely reinvent the nice form functionality in Formik just to be able to handle both use cases. Do you have any additional thoughts on this?
Also, I would definitely love to hear your opinion on why you submit the entire settings object rather than PATCH for your REST API. I'm imagining a use case where the settings object might be rather large, and you'd be submitting a bunch of unrelated fields along with the "1" you changed, especially if you're firing off auto-saves one after another.
Tbh. Another valid solution would be to make each settings item itโs own form.
@jaredpalmer Yea, that's an interesting thought. I think I was trying to avoid having a ton of repetitive markup something like <SettingsField onSubmit={submitHandler} {...} />
, especially since I usually end up putting props on their own lines. Same concept as why you have a <Field />
component to reduce boilerplate -- to avoid having a million props like value
, onChange
, onBlur
, etc on every single field. It's always a double-edged sword, lol.
Hi. Using react-native-material-textfield this is all I had to do to add validation and submission per an individual field.
const [feeValidationError, setFeeValidationError] = useState('');
const personalFeeValidation = Yup.number()
.typeError('The fee must be a number.')
.required(
`The fee override is not set because the field is empty. Please enter a number or switch back to practice default.`
)
.min(0, `The fee cannot be less than zero.`);
<TextField
onEndEditing={async (event) => {
try {
const text = event.nativeEvent.text;
const validation = await personalFeeValidation.validate(text);
dispatch(patchFee(text, false));
console.log(text);
setFeeValidationError('');
} catch (e) {
console.log(e);
setFeeValidationError(e.message);
}
}}
error={feeValidationError}
placeholder='$0.00'
prefix='$'
label='Personal fee override for evisits'
keyboardType={'number-pad'}
value={`${state.home.providerPrice}`}
enablesReturnKeyAutomatically={true}
/>
As you can see, the boilerplate is quite minimal. It's really just:
It seems to me that it would be much easier either adding a custom hook or enhancing the textinput of your choice with this boilerplate rather than adding it onto Formik itself. As a matter of fact it seems because Formik is about ONE form, it actively gets in the way of this type of functionality.
And here is a ready made component that does exactly this. You simply pass in a Yup validation and an onValidationSuccess callback function.
import React, { useState } from 'react';
import { TextField, TextFieldProps } from 'react-native-material-textfield';
import * as Yup from 'yup';
interface iValidatedInput extends TextFieldProps {
validation: Yup.MixedSchema;
onValidationSuccess: (inputText: string) => void;
}
export const ValidatedInput = (props: iValidatedInput) => {
const [validationError, setValidationError] = useState('');
return (
<TextField
onEndEditing={async (event) => {
try {
const text = event.nativeEvent.text;
await props.validation.validate(text);
setValidationError('');
props.onValidationSuccess(text);
} catch (e) {
console.log(e);
setValidationError(e.message);
}
}}
error={validationError}
{...props}
/>
);
};
Used like this:
const personalFeeValidation = Yup.number()
.typeError('The fee must be a number.')
.required(
`The fee override is not set because the field is empty. Please enter a number or switch back to practice default.`
)
.min(0, `The fee cannot be less than zero.`);
const personalFeeOnSuccessCallback = (inputText: string) => {
dispatch(patchFee(parseInt(inputText), false));
};
<ValidatedInput
validation={personalFeeValidation}
onValidationSuccess={personalFeeOnSuccessCallback}
placeholder='0.00'
prefix='$'
label='Personal fee override for evisits'
keyboardType={'number-pad'}
value={`${state.home.providerPrice}`}
enablesReturnKeyAutomatically={true}
/>
Let me know what you think.
@shamilovtim That sounds pretty reasonable, and would separate all this from Formik. I will experiment with this asap as we're working on the project and respond here when I get a chance. Thank you!
I'm currently working on a similar auto-save concept with Rest Patch. I need to validate the entire form before moving to the "next page". Still, I want to auto-save on every onBlur.
Here's a suggestion:
const [lastBlurKey, setLastBlurKey] = React.useState(null);
const [lastBlurValue, setLastBlurValue] = React.useState(null);
const [blurObject, setBlurObject] = React.useState(null);
const handleHandleBlur = (event: React.ChangeEvent<HTMLInputElement>) => {
const newBlurObject = {
[event.target.id]: event.target.value,
};
if (lastBlurKey !== event.target.id || lastBlurValue !== event.target.value) {
setLastBlurKey(event.target.id);
setLastBlurValue(event.target.value);
setBlurObject(newBlurObject);
}
handleBlur(event);
};
React.useEffect(() => {
if (blurObject) {
// Do something with object, like Rest Patch
}
}, [blurObject]);
Here is how I implement partial save:
useState to control the validation function to be applied for name field. validation is enabled except 'SAVE' button OnClick been triggered. In SAVE OnClick, change state of validation function to no-op. When isSubmitting becomes false, meaning submitting is done, change validation state back to regular validation.
Also I use isSaveButton initialValue combined with setFieldValue to keep track which button 'SAVE' or 'SUBMIT' been clicked.
let initialValues = {
name: '',
isSaveButton: false
};
const [validateName, setValidateName] = useState(() => value => {
let error;
if (!value) {
error = 'Event Name is required.';
}
return error;
});
<Formik
enableReinitialize={true}
initialValues={initialValues}
onSubmit={(values, actions) => {
values.isSaveButton
? saveHandler(values)
: submitHandler(values);
actions.setSubmitting(false);
if (!actions.isSubmitting) {
setValidateName(() => value => {
let error;
if (!value) {
error = 'Event Name is required.';
}
return error;
}}
}>
{({
errors,
handleBlur,
handleChange,
setFieldValue,
touched,
values
}) => (
<Form>
<Field
id="name"
name="name"
type="text"
validate={validateName}
onChange={handleChange}
value={values.name}
onBlur={event => {
handleBlur(event);
}}
/>
{touched.name && errors.name && (
<div >
{errors.name}
</div>
)}
<Button
type="submit"
onClick={e => {
setFieldValue('isSaveButton', true, false);
setValidateName(() => () => {});
}}>
Save
</Button>
<Button
type="submit"
onClick={e => {
setFieldValue('isSaveButton', false, false);
handleSubmit(e);
}}>
Submit
</Button>
</Form>
</Formik>
๐ Feature request
Current Behavior
Currently, Formik doesn't seem to have quite enough extensibility to implement any sort of Auto-Save or Partial Submit functionality, mostly due to lack of accessibility to a couple internal props.
Desired Behavior
Ability to: Implement Auto-Save (both single value and whole form):
autoValidateOnChange = false
, runvalidateField
manually and use resulting promise to trigger auto-save of that one value (not the whole form). Currently, there's no way to hook into ahandleChange
promise, triggervalidateField
manually and submit that single value if valid (aka. Partial Submit).Why Partial Submit?
Use Case: React Native Settings Screens: http://zsl.io/ZB5KYH
This is basically a
PATCH
operation, where all values go to the same API endpoint, in the same manner, so it makes sense to have them all within the same form so we can take advantage of all of Formik's state and validation features. Obviously we can't wrap every field in a separate Formik form; that would be obnoxious, lol. Also, this is important to avoid having to submit the ENTIRE object of settings on everyhandleChange
; it would be a network performance issue, and increase the chance of settings getting overwritten in the event that the server changes an unrelated setting, then the app submits the ENTIRE settings object right after.I've also seen people in Formik's issues needing this same functionality for Draft Auto-Save.
Suggested [Potential] Solutions
Return a Promise from
handleChange
andhandleBlur
:autoValidateOnChange = true
, return a Promise after validation is complete, otherwise aftersetState
for the value is complete. This is needed because runningvalidateField
depends on that being complete in order to run it by hand. Related Issue: #1349Who does this impact? Who is this for?
Anyone:
handleChange
instead of the whole formhandleChange
orhandleBlur
is done doing its thing and the form values have been updated.Describe alternatives you've considered
I'm currently performing this hackish workaround:
handleChange
generator: http://zsl.io/6lNMY4 & http://zsl.io/wMov5MisValidating
state: http://zsl.io/PHHVB7Field
'svalidate
prop at all: http://zsl.io/n2knqhIt's depressing, really ๐ข
Additional context
I've also tried using the
Field
component to provide avalidate
function, etc, but because of the lack of synchronous nature of runninghandleChange
, thenvalidateField
, I'm not able to take advantage of reducing boilerplate there either.This is extremely important to me, and can be accomplished with very little change to Formik, by only exposing a couple small things (ie. Promise or hook). I've currently had to write my own solution for a ton of this, and the code is quickly turning into a duplicate of Formik, which pains me ๐.
I've also looked through all the v2 code to see if there's anything to help there, but I'm not finding anything. I also looked through all of the issues to make sure I wasn't creating a duplicate. See below.
Other issues that could be related or useful:
1298 - How to call handleSubmit on a particular set of fields?
1218 - How to trigger form submit on change
handleChange
PLEASE!1203 - Won't solve the problem, but was an issue when I was trying to validate manually and didn't have access to the specific field ref.
1046 - Hooks rewrite
529 & #1398 Both reference a lot of people's needs for handlers/actions returning promises
If anyone has suggestions, or I'm missing something, please let me know!