jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.95k stars 2.79k forks source link

onChange for entire form #271

Closed timoxley closed 6 years ago

timoxley commented 6 years ago

I have a form where some field's values depend on other fields values. I have been using redux-form and have been able to calculate these values using an onChange handler on the top-level form object.

I don't see an equivalent using formik, is this right? I thought I could hijack the validation function to do this, but it's not passed the formik bag, only the values to validate.

Another usecase for a form-level onChange is automatic persistence without needing to hit a submit button e.g. something like:

<Formik
  initialValues={JSON.parse(localStorage.getItem('formvalues'))}
  onChange={(values) => {
    localStorage.setItem('formvalues', JSON.stringify(values))
  }}
>
{/* … */}
</Formik>

Another option could be a "submitOnChange` flag.

prichodko commented 6 years ago

You will always receive latest values in render prop. So you can perform anything you need there. I also recommend having a look at https://github.com/jaredpalmer/formik-persist how such functionality could be implemented. Take a note that <FormikPersist /> takes values from the context, you can of course pass it as prop instead.

jaredpalmer commented 6 years ago

@prichodko is exactly correct.

benvan commented 6 years ago

Sorry to reopen a closed thread, but this seemed like the most appropriate place to follow up on this.

If I understand correctly, the suggestion is to use another component to simulate onChange responses. I can see how that might make sense for something extremely agnostic like FormikPersist, but for the general situation where you wish to cause some effect based on form changes, this doesn't make sense to me.

Consider a simple form which contains dropdowns for the ingredients of a burger. You wish to render a preview of the burger outside the form somewhere. It would seem fairly simple (and conventional) to do this by passing the form an "onBurgerIngredientsChanged" handler, and hooking that up into the form's onChange handler.

Am I correct in understand you would suggest instead creating a <BurgerIngredientsChangedDetector /> component, complete with constructor, componentDidMount, componentWillReceiveProps methods ...

It seems to me this would very naturally lead people to creating their own <OnFormUpdate hander={someHandler}/> component. In this case... why the extra complexity? Would it not be better to be able to pass Formik an onChange handler?

Just my two cents - I'm facing this problem now with a few forms that are slightly more dynamic than their siblings. Any thoughts would be much appreciated.

Thanks

Edit: Furthermore - and I think this is the bigger issue - by binding "onChange" type handlers into the render lifecycle, you're coupling two very different concepts. My form might render itself every second if it's being passed a readonly "counter" value - this doesn't mean any of the values have changed!

sergioramos commented 6 years ago

In case someone stumbles upon this thread, I've written a lib for this use case: https://github.com/ramitos/formik-observer

Grsmto commented 6 years ago

Hey, I have been using Formik on 2 production projects now and I have needed this feature for both. At the moment I hijacked the validation method to trigger the onChange. I feel like this would be worth considering as a built in feature? You can't trigger side-effects in render() so this is required if you want to trigger any kind of side-effect. An onChange callback would just avoid user to have to build a special library for every single side-effect.

These side-effects could be:

Also notice how these usages are actually the only reason why one would use Redux-Form over Formik. Once you have that onChange callback, you can really easily save the state in Redux if you want to, which nullify the need of using Redux-Form and allow all the usages I said above.

jaredpalmer commented 6 years ago

See #401

brotzky commented 6 years ago

Not something I'm proud of, but I hooked into the validate prop to get access to the form values on every change 😏

// Rough sketch

class Component {

  validate = values => {
    // Do what you want with the values
    console.log(values)
    return {}
  }

  render() {
    <Formik validate={this.validate} />
  }
}

⚠️ I wouldn't recommend, but it might save some people that are in a pinch. Use my creative hacks at your own risk.

mcspud commented 6 years ago

@ramitos - Great little package, perfect for what I needed it for. Thanks

vsamotskiy commented 5 years ago

Founded some interesting solution of this issue. ` {({ values }) => (

{this.handleWholeFormChange(values)}
)}

`

rizwanahmed19 commented 5 years ago

@vsamotskiy What if I wanna update my component state in this.handleWholeFormChange(values)?

brynner commented 5 years ago

I can't believe why I can't use properly and simply the onChange property.

rohanBagchi commented 5 years ago

Added a notifyOnChange prop to formik. This should solve the form level onChange issue. https://github.com/jaredpalmer/formik/pull/1397

PS: for now I have @brotzky 's creative hack in prod :)

FerreiraRaphael commented 5 years ago

Since notifyOnChange hasn't been merged yet, I did this little hack for observing formik values.

Component

import React, { useEffect } from 'react'

interface Props<T> {
  onChange: (value: T) => void
  value: T
}

export function FormikObserver<T>(props: Props<T>) {
  useEffect(() => {
    props.onChange(props.value)
  }, [Object.values(props.value).join(', ')])
  return null
}

FormikObserver.defaultProps = {
  onChange: () => null,
}

Usage

<Form>    
{(formProps: FormikProps<{}>) => {
    return (
       <Fragment>
         // Your form
         <FormikObserver value={formProps.values} onChange={value => console.log(value)} />
       </Fragment>
     )
   }}
</Form>
ebrearley commented 5 years ago

What about doing something like this? Which I think is what @prichodko was saying in https://github.com/jaredpalmer/formik/issues/271#issuecomment-347142295

<Formik initialValues={initialValues} onSubmit={onSubmit}>
  {({ values }) => (
    <Form onChange={() => onFormChange(values)}>
      {/* Input fields */}
    </Form>
  )}
</Formik>

I just ignore the synthetic event that's piped into the <Form /> onChange event handler and replace it with the values dictionary from the <Formik /> value render prop. Works like a charm in my project 😄

Didn't need any Observable third party implementations.

dragosdehelean commented 5 years ago

@ebrearley: it doesn't work for React Native as there is no <Form /> component there.

My use case: I need to verify isValid after each change of all the pseudo-form's inputs and trigger an event outside Formik. In the render method isValid is not stable (!) The only place where it's stable is on the onChange event of one of the inputs. So the only solution for me was to listen for isValid in each onChange event of each input...

I think a better solution is more than needed. At least for React Native.

johnrom commented 5 years ago

@dragosdehelean the proper way to handle something like this is to use an effect. An example of this implementation can be seen in:

I'm hoping to add some examples to the documentation in #1787. Let me know if this helps!

Etheryte commented 4 years ago

I don't agree that a separate component with an effect is the reasonable architecture. This is essentially diverting from the original (and reasonably sane) form configuration via props to configuration via XML.

You don't respond to the form submit event by adding a component to your form:

<Form>    
{(props) => (
    <>
        ...
        <SubmitObserver value={props.values} onSubmit={this.onSubmit} />
        ...
    </>
)}
</Form>

Why would change be different?

Logically both submit and change are simply one of many events that a form may emit, just like reset or validate.
I fail to see why different events should have so wildly different approaches in canonical use.

johnrom commented 4 years ago

@Etheryte I absolutely agree with your argument that it's not intuitive. However, I do think a simple onChange wouldn't provide enough flexibility for all user scenarios, and if we don't find a way to provide for all user scenarios, there isn't a strong case to expand the API. Feel free to open a PR if you have a solution that can both optimize for renders when states are identical and allow users to circumvent this optimization if needed in a way that is clear to users. I haven't looked much into the v2 api so maybe there's a simple way now that we have hook support.

Edit: the "Form Submit Event" example doesn't really exist in React Native so it doesn't apply to the full audience of Formik.

sgronblo commented 4 years ago

Couldn't you just have two separate callbacks? onValuesChanged and onNewValidValues or something?

valoricDe commented 3 years ago

As this thread is still quite high up in the google search I post the unnecessary complex but new way here: https://formik.org/docs/api/useFormikContext

azhararmar commented 3 years ago

@valoricDe thanks, works perfectly

thushara5884 commented 3 years ago

Guys, i found a better solution

<Formik>
{
    (values) => {
       useEffect(() => {
          this.formOnChange(values);
       }, [values]);
       return ( 
          <div>test</div>
       )
    }
}
</Formik>
Etheryte commented 3 years ago

@thushara5884 Using useEffect() in a callback is not valid and is one of the classic examples of mistakes to avoid with hooks. If you check your browser console with the above example, you should find that you have an error warning you about this: "Invalid hook call. Hooks can only be called inside of the body of a function component.".

thushara5884 commented 3 years ago

@Etheryte yes you are correct. i was hoping formik internally uses children as another component, but it seems it use it as a callback function.

: children // children come last, always called
        ? isFunction(children)
          ? (children as (bag: FormikProps<Values>) => React.ReactNode)(
              formikbag as FormikProps<Values>
            )
thushara5884 commented 3 years ago
<Formik>
{
    (values) => {
       const FormValuesListener = () => {
        useEffect(() => {
            this.handleFormOnChange(values);
        }, [values]);
        return null;
    }
        return (<Form>
            <FormValuesListener  />
        </Form>)
    }
}
</Formik>

This is the best solution i found so far.

ecarrera commented 3 years ago

What about doing something like this? Which I think is what @prichodko was saying in #271 (comment)

<Formik initialValues={initialValues} onSubmit={onSubmit}>
  {({ values }) => (
    <Form onChange={() => onFormChange(values)}>
      {/* Input fields */}
    </Form>
  )}
</Formik>

I just ignore the synthetic event that's piped into the <Form /> onChange event handler and replace it with the values dictionary from the <Formik /> value render prop. Works like a charm in my project 😄

Didn't need any Observable third party implementations.

I was ignoring this. I didn't knew how to get global events from <Formik> component. Thanks @ebrearley 🎉 <Form> component is huge of props and events ❤️

acuthbert commented 2 years ago

What about doing something like this? Which I think is what @prichodko was saying in #271 (comment)

<Formik initialValues={initialValues} onSubmit={onSubmit}>
  {({ values }) => (
    <Form onChange={() => onFormChange(values)}>
      {/* Input fields */}
    </Form>
  )}
</Formik>

I just ignore the synthetic event that's piped into the <Form /> onChange event handler and replace it with the values dictionary from the <Formik /> value render prop. Works like a charm in my project smile

Didn't need any Observable third party implementations.

Did anyone else find that this technique was always one change behind? In other words, the form's onchange is being called before formik has collected the change into the values object? In my case I need this for an autosave - luckily this was being debounced anyway so the delay wasn't an issue but I could see how it would be....

I don't understand their hesitancy in adding a context level onChange event. With Formik's approach being to collect and manage the form state at the highest level it seems fitting that it could report when the form is changing.

I have reviewed the approaches taken which boil down to:

1) Use a useEffect, and watch the values array for changes yourself. Not a great solution - I'm sure formik is already doing something like that internally. Adding another slow value comparison for large forms feels wrong 2) Use onChange on the form, and debounce the callback - introduce a delay to be sure values is fully up to date. If you need instance updates however you'll end up tuning that delay and getting race conditions on slow devices 3) Intercept onChange events from all of your form fields manually. The most 'correct' but completely unwieldly and opposite to the advantages of formik in the first place. It also doesn't work well with dynamic form generation or highly nested presentation layers.

Of course the issue here could be that formik internally is unable to easily tell when it's dispatch of new state to React has actually taken effect - so I'd expect it to have to monitor the values object itself - it just feels like it would be better for the library to do this...

johnrom commented 2 years ago

@acuthbert it shouldn't be one render behind, by nature of effects being 1-to-1 with renders. it could be triggering your effect multiple times, like when validation is complete, etc, and so triggering

onChange(1)
(change updated to 2 before validation completes)
validation, onChange(1)
onChange(2)

Or your debounce fn could be using a stale callback. Formik doesn't have validation cancellation when the changes are stale, but unless validation is async it will always run in order. If you use async validation it's possible it would do something like onChange(1), onChange(2), validation(1) + onChange(1), validation(2) + onChange(2) in v2. You should determine whether validation is causing an onChange and determine why (it shouldn't if useEffect dependencies are limited to values). If you create a sandbox repro I can take a look.

3231 sets up the foundation for an onChange prop and validation cancellation, as it enables optionally subscribing to state. Generally the Form component wouldn't subscribe to state because it doesn't need any, but with onChange it would need to. The issue with onChange is, how do we build it in an extensible way that applies to everyone's situation? Just adding a hook dependency to values seems like it would work for some and not others. Ultimately I think even if we implemented it, you'd want to create your own component that does the same thing for any advanced logic.

Here's the de facto ChangeHandlingForm for v2. Notice it is impossible for values to become stale because the onChangeFn doesn't use values from outside the scope. If you pass values from outside the scope, it could become stale, so make sure those deps are passed to useEffect.

interface ChangeHandlingFormProps<Values> {
  onChange: (values: Values) => void;
}
const ChangeHandlingForm = <Values,>(props: PropsWithChildren<ChangeHandlingFormProps<Values>>) => {
  // in v3 this would be `const { values } = useFormikState<Values>();`
  const { values } = useFormikContext<Values>();

  useEffect(() => {
    // you might modify this to fit your use case
    // like adding a `const prevValues = usePrevious(values)` above and checking for equality
    // or debounce this whole function but make sure not to use stale values
    props.onChange?.(values);
  }, [props.onChange, props.onChange && values]);

  // Formik's Form component
  return <Form>
    {props.children}
  </Form>
}

const MyForm = () => (
  <Formik {...formikConfig}>
    <ChangeHandlingForm onChange={values => console.log(values)} />
  </Formik>
);
tom-wagner commented 2 years ago

So using the validate() hack results in validate being invoked multiple times. And the FormikObserver library looks interesting, but a google search shows me it has never been publicly used in combination with useFormik

Any ideas out there for me to implement a form-level onChange within useFormik()?

image

image

Edit: I ended up using a similar approach to the FormikObserver library, but hacking it together by hand:

validate: async (values: any) => {
      // TODO: consider turning on loader, then delaying API call 
      // in order to reduce the number of API calls to backend

      const stringified = JSON.stringify(values)
      if (stringified !== stringifiedFormValues) {
        setStringifiedFormValues(stringified);

        console.log("running validate!!");
        await debouncedUpdate(RequestType.Analyze, { values });
      }
    },
MelMacaluso commented 2 years ago

It seems that callin a function inside the render function with the values inside is always one step behind as mentioned.

Using the onChange method on <Form /> results in unexpected results.

So far the most solid way to accomplish that is creating a component that listens to changes according to the argument values of the render function and add a callback as prop, such as:

const FormikListener = ({ values, callback }) => {
  useEffect(() => {
    callback(values);
  }, [callback, values]);

  return null;
};

and inside the render function:

...
  <FormikListener values={values} callback={handleOnChange} />
...
Danielvandervelden commented 2 years ago

@MelMacaluso ended up doing the same... Values weren't updating on the internal Form element's onChange. Everytime it was 1 step behind. This seemed like the cleanest solution.

MelMacaluso commented 2 years ago

@MelMacaluso ended up doing the same... Values weren't updating on the internal Form element's onChange. Everytime it was 1 step behind. This seemed like the cleanest solution.

I think is very flaky and horrible but I had to get going, I wish I could hear take of the lib's creator so we do what is intended instead. @jaredpalmer 🙏🏻

worldomkar commented 1 year ago
<Formik>
{
    (values) => {
       const FormValuesListener = () => {
      useEffect(() => {
          this.handleFormOnChange(values);
      }, [values]);
      return null;
  }
        return (<Form>
            <FormValuesListener  />
        </Form>)
    }
}
</Formik>

This is the best solution i found so far.

Ideally, onChange handler was expected to be already implemented - which wasn't case in 2023.

As a quick workaround, I brought in external state inside the form (which is very specific to use case I have). This is the least ugly, highly familiar and logical available solution that worked perfectly just in time.

nikhil-swamix commented 7 months ago

indirect thanks to community, i was going through this and figured for my problem in svelte, it was showing up in google results, so for anyone looking to trigger on any child of form changed, just do this. image

CosmaTrix commented 6 months ago

The <FormikLister /> solution is a nice one, but it has a caveat. Because functions are always different across renders (the callback prop in this case) the useEffect is going to get triggered everythime the parent re-renders.

const FormikListener = ({ values, callback }) => {
  useEffect(() => {
    callback(values);
  }, [callback, values]); // <- dependencies always change because callback is always different

  return null;
};

That can cause cause an infinite loop if, for example, the callback is doing some async logic and changing the state to show/hide a loader. It can be easily fixed by wrapping the function passed to the callback property in useCallback, but it is easyly forgettable and can be a potential pitfall.

const handleOnChange = useCallback((values) => {
  console.log(values)
}, []);

<FormikListener callback={handleOnChange}>
lukebrandonfarrell commented 4 months ago

The <form> approach does not work on mobile. I'm not sure why this is marked as closed, as it remains a significant pain point for all projects using Formik. I'm very tempted to switch to another form library at this point.

Nevermind, seems like you can use formik-effect, thank you :)

Ok, back again 😅 so formik-effect didn't work, seems like the library is outdated, in the end I used a custom component for this:

export function FormikObserver<V>({ onChange } : FormikObserverProps<V>) : null {
  const { values, initialValues } = useFormikContext<V>();
  const previousValues = useRef(initialValues);

  useEffect(() => {
    if (!_.isEqual(previousValues.current, values)) {
      previousValues.current = values;
      onChange(values);
    }
  }, [values, initialValues, onChange]);

  return null;
}