Formik: RFC: Controlled Formik (a.k.a top-level onChange prop)

Created on 6 Aug 2018  路  14Comments  路  Source: formium/formik

Current Behavior


Formik is an uncontrolled component. You can set default values through initialValues/mapPropsToValues and then it's a black box until submit happens. You can use FomikEffect to trigger side effects underneath.

Desired Behavior


Give up. Let people get silly. Allow them to store their own parts of Formik state.

class ControlFormikValues extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
    };
  }

  render() {
    return (
      <Formik
        values={this.state}
        onChange={({ values }, state) => this.setState(values)}
      >
        <Field name="name" />
      </Formik>
    );
  }
}

Suggested Solution

import React from 'react';
import { FormikContext } from './FormikContext';

function cbToCb(cb) {
  return typeof cb === 'function' ? cb : noop;
}

function noop() {}

export class Formik extends React.Component {
  handleChange = e => {
    if (e.persist) {
      e.persist();
    }
    this.internalSetState(state => ({
      values: { ...state.values, [e.target.name]: e.target.value },
    }));
  };

  handleBlur = e => {
    if (e.persist) {
      e.persist();
    }
    this.internalSetState(state => ({
      touched: { ...state.touched, [e.target.name]: true },
    }));
  };

  state = this.getState({
    values: this.props.initialValues,
    errors: this.props.initialErrors,
    touched: this.props.initialTouched,
    isSubmitting: false,
    isValidating: false,
    submitCount: 0,
    handleBlur: this.handleBlur,
    handleChange: this.handleChange,
  });

  getState(stateToMerge = this.state) {
    return Object.keys(stateToMerge).reduce((state, key) => {
      state[key] = this.isControlledProp(key)
        ? this.props[key]
        : stateToMerge[key];
      return state;
    }, {});
  }

  isControlledProp(key) {
    return this.props[key] !== undefined;
  }

  internalSetState = (stateToSet, cb) => {
    let onStateChangeArg = {};

    const isStateToSetFunction = typeof stateToSet === 'function';
    return this.setState(
      prevState => {
        prevState = this.getState(prevState);
        let newStateToSet = isStateToSetFunction
          ? stateToSet(prevState)
          : stateToSet;
        let nextState = {};
        Object.keys(newStateToSet).forEach(key => {
          if (prevState[key] !== newStateToSet[key]) {
            onStateChangeArg[key] = newStateToSet[key];
          }

          if (!this.isControlledProp(key)) {
            nextState[key] = newStateToSet[key];
          }
        });

        return nextState;
      },
      () => {
        cbToCb(cb);
        if (this.props.onChange !== undefined) {
          this.props.onChange(onStateChangeArg, this.getState);
        }
      }
    );
  };

  render() {
    return (
      <FormikContext.Provider value={this.getState()}>
        {/* ... */}
      </FormikContext.Provider>
    );
  }
}

Who does this impact? Who is this for?


Errrrr'body.

Describe alternatives you've considered

Not doing this.

Additional context

RFC stale v2

Most helpful comment

Keeping/managing form state isn't the only use case of a top-level onChange. Sometimes there are fields which depend on the content of other fields.

For example, and it's common use case I would think, I might need to reset certain fields of a form after a certain select field changes, without touching the rest of the form.

I can't find a simple way to implement behaviour like that in the current version of Formik.

Related issues: #485 #401 #271

All 14 comments

Well, thats exacly what I want. (Case : react-credit-cards)

Why do we need to keep the internal state?

@prichodko because you don't need to make _all_ of Formik's state controlled, only the parts you need to. So in the above example, values is controlled, but the rest of Formik state is uncontrolled (kept in Formik).

I was just having a conversation with a colleague about this. So glad it's maybe coming!

I really need this feature :)

I think it should be a fully controlled component. I'm working with multiple forms at once and I just want to let user see exactly what they left off, the form will affect each other as well

Hola! So here's the deal, between open source and my day job and life and what not, I have a lot to manage, so I use a GitHub bot to automate a few things here and there. This particular GitHub bot is going to mark this as stale because it has not had recent activity for a while. It will be closed if no further activity occurs in a few days. Do not take this personally--seriously--this is a completely automated action. If this is a mistake, just make a comment, DM me, send a carrier pidgeon, or a smoke signal.

ProBot automatically closed this due to inactivity. Holler if this is a mistake, and we'll re-open it.

Keeping/managing form state isn't the only use case of a top-level onChange. Sometimes there are fields which depend on the content of other fields.

For example, and it's common use case I would think, I might need to reset certain fields of a form after a certain select field changes, without touching the rest of the form.

I can't find a simple way to implement behaviour like that in the current version of Formik.

Related issues: #485 #401 #271

@nyanpasu Aren't you simply looking for https://github.com/jaredpalmer/formik-effect for that?

Yes, but the problem is https://github.com/jaredpalmer/formik-effect doesn't work with formik anymore. Please refer to: https://github.com/jaredpalmer/formik/issues/766

Why not having both? I.e. do it like inputs in React.
You either provide values and an onChange handler and control the value of the form values yourself.
Or you just provide initialValues and let formik control the form values.
(I guess it's fine to keep state about dirtiness, errors, etc always within formik)

I've used this pattern in react-reform and was able to apply a lot of complex logic thanks to this pattern.

As I'm not too happy about react-reform's validation API, I'm thinking of switching to formik for some projects and was quite surprised how difficult some things are to translate due to the lack of this pattern. Happy to create a new issue if you feel this isn't the right place to discuss this!

I personally wouldn't mind the ability to inject a controlled reducer, something that runs alongside the internal reducer. I'm thinking about forking a proof of concept.

hello there

my use case is this:

Formik forms are inside a Redux provider, as well as other non-formik (not forms) parts of the same app.
Each time values or dirty props from withFormik change, I need to lift them up dispatching actions so other places have access.

Looks like Formik Observer provides a way but I'm not sure I'm able to add this dep. to this project

So, a valid workaround could be to add a useEffect, where I check if values and dirty are different from their previous value, then dispatching them into some action.

Any thoughts? thanks!

@tincho like you said, you can useEffect with the Formik v2 API like this:

```jsx
const MyForm = ({ syncValuesToRedux }) => {
const formik = useFormik({
initialValues: { firstName: '' },
onSubmit: () => {}
});

// if using eslint, you'll have to disable eslint's rules-of-hooks/exhaustive-deps here
// if you don't want to trigger this effect when syncValuesToRedux changes
// it's probably memoized anyway, but doing it this way will prevent infinite renders when syncValuesToRedux isn't memoized.
useEffect(() => {
syncValuesToRedux(formik.values);
}, [formik.values]);

return




;
}

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Jucesr picture Jucesr  路  3Comments

pmonty picture pmonty  路  3Comments

PeerHartmann picture PeerHartmann  路  3Comments

sibelius picture sibelius  路  3Comments

emartini picture emartini  路  3Comments