jaredpalmer / formik

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

add setInitialValues to useFormikContext and setInitialValue to useField #2814

Open mainfraame opened 4 years ago

mainfraame commented 4 years ago

🚀 Feature request

Add the ability to update the initialValues within a child component.

Current Behavior

The only way to update the initial values is by updating the initialValues prop with enableReinitialize true via the

element or useFormik hook.

Desired Behavior

at the field level by adding a "setInitialValues" helper method to the useFormikContext hook and a "setInitialValue" to the useField hook.

Suggested Solution

Modify the FormikHelpers structure to include a setInitialValues and setInitialValue methods; assuming this means adding these helpers to the context and updating the state reducer. Method signatures should look something like this:

function setInitialValue<T=any>(field: string, value: T): void;

function setInitialValues<T=any>(initialValues: ((prevInitialValues) => T) | T): void;

eg:

import React, {useEffect} from 'react';
import {getIn, useField, useFormikContext} from 'formik';

const Input = (props: {initialValue: any; name: string}) => {
    const field = useField(props.name);

    useEffect(() => {
        field.setInitialValue(props.initialValue);
    }, [props.initialValue]);

    return (
        <input
            onChange={({target: {value}}) => field.setValue(value)}
            value={field.value}
        />
    );
};

// setInitialValues

type FormSectionProps<T = any> = {
    initialValues?: {
        [name: string]: T
    };
    inputs: {
        name: string;
    }[];
}

const FormSection = (props: FormSectionProps) => {
    const formik = useFormikContext();

    useEffect(() => {
        formik.setInitialValues((prev) => ({
            ...prev,
            ...props.initialValues
        }));
    }, [props.initialValues]);

    return (
        <>
            {props.inputs.map((input) => (
                <input
                    key={input.name}
                    onChange={({target: {value}}) => formik.setFieldValue(input.name, value)}
                    value={getIn(formik.values, input.name)}
                />
            ))}
        </>
    );
};

Who does this impact? Who is this for?

In a couple projects that I've used formik on, I've often ran into a issue where it's only possible to update initial values by setting enableReinitialize to true and updating the initialValues prop that is passed into the Form component.

Describe alternatives you've considered

I've used an additional context wrapping my base Form component to update the initialValues via a child component, but I feel this introduces useless re-renders and would be better handled as part of the formik context.

clflowers5 commented 4 years ago

Curious, what's the actual use-case here? Are you rendering the form, then re-initializing/rendering it with values from an API? I can't think of any other scenario offhand where you'd want to modify initialValues.

lucasreppewelander commented 4 years ago

I have an use-case where I have this user form where users will add personal information about them selves, as the app is an SaaS application different customers can have enabled/disabled certain fields in their settings, which means that customer A can have 3 fields of name, location and interests and customer B can have their form consisting of name, location, car and ice contact, for example

which means I can't have a initialValues consisting of the entire possibilities of the form as I don't want to render those input fields that the user in question shouldn't be able to/isn't allowed to edit.

clflowers5 commented 4 years ago

In the case of dynamic forms (what you're describing to my understanding) I find it's much easier to just not render the form until you have all the required data.

That prevents scenarios like UI flicker/flashing, and is way easier to manage when it comes to testing. I don't see how updating the initialValues of Formik would actually help in this scenario.

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

offirgolan commented 3 years ago

I'd like to add a +1 to this feature request.

We have a current use case where we have user managed configurations for modifying certain views (e.g. add/remove columns, sort direction, filters, etc.) that is fully controlled by a single formik state. When the user is done making changes to the config via different forms / table actions, they can save their new config.

This becomes problematic when we want to do single time operations to save a part of the config without any other changes (e.g. change the name of their config). Ideally we'd like to save the new name and then set the new name as part of the initial values so that the form would no longer be considered dirty from the name change since its already been updated in the DB.

Banking on enableReinitialize isn't ideal since it resets the user's current changes (e.g. they've removed a few columns and then decided to change the name). Having something like function setInitialValues<T=any>(initialValues: ((prevInitialValues) => T) | T): void; would be incredibly useful is a situation like this.

RomanKwok commented 3 years ago

I'd like to add a +1 to this feature request.

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

konrazem commented 3 years ago

I'd like to add a +1 to this feature request. I have many dependent Formiks in my app and I can not use one only. I would like to reinitialize certain values for one formik when the other is changing.

efreed commented 3 years ago

+1

github-actions[bot] commented 3 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

juliusdejon commented 3 years ago

+1

epashkov commented 2 years ago

+1 Another use case is when i want to initialize field value with GraphQL fragment data. With current implementation we cannot divide form query for fragments if we want.

konrazem commented 2 years ago

@epashkov What I did I use pick/omit and marge from lodash

epashkov commented 2 years ago

@konrazem Could you please describe what do you mean?

Suppose I have a one big form and initial values for this form comes from api:

We have root fragment to get initial values for whole form

const fragmentSpec = graphql`
  fragment UserProfileForm_query on Query
  @argumentDefinitions(userId: { type: "ID!" }) {
    profile(id: $userId) {
      id
      subscription
    }
  }
`;

const UserProfileForm = (props) => {
  const data = useFragment(fragmentSpec, props.query);

  const handleSubmit = (data) => { ... };

  const initialValues = { subscription: data.user.subscription };

  return (
    <Formik
       initialValues={initialValues}
       onSubmit={handleSubmit}
     >
       {props => (
         <form onSubmit={props.handleSubmit}>
           {/* Component with subscription field */}
           <SubscriptionFormPart />
           <button type="submit">Submit</button>
         </form>
       )}
     </Formik>
  );
};

We want to move fragment definition to Subscription form component and use React Suspense capabilities to handle load state for initial value for other parts of the form. Like this:

SubscriptionFormPart.tsx

const fragmentSpec = graphql`
  fragment SubscriptionFormPart_profile on Profile
  {
    subscription
  }
`;

const SubscriptionForm = (props): JSX.Element => {
  const profile = useFragment(fragmentSpec, props.query);

  {/* And initial values for this part of the form here */}
  const { setInitialValues } = useFormicContext();

  useEffect(() => {
    setInitialValues({ subscription: profile.subscription });
  });

  return <Field name="subscription" />;
};

And parent component fragment will looks like

const fragmentSpec = graphql`
  fragment UserProfileForm_query on Query
  @argumentDefinitions(userId: { type: "ID!" }) {
    profile(id: $userId) {
      id
      ...SubscriptionFormPart_profile
    }
  }
`;
epashkov commented 2 years ago

I think that another way to do thi is using setFieldValue() while each component is mounted like this

const SubscriptionForm = (props): JSX.Element => {
  const profile = useFragment(fragmentSpec, props.query);

  const { setFieldValue } = useFormicContext();

  useEffect(() => {
    setFieldValue('subscription', profile.subscription, false);
  }, [setFieldValue, profile]);

  return <Field name="subscription" />;
};

Is this implementation has any negative side effect?