4
votes

I use a formik form with forward refs like so

Form.js

import React from "react";
import FormikWithRef from "./FormikWithRef";

const Form = ({
  formRef,
  children,
  initialValues,
  validationSchema,
  onSubmit
}) => {
  return (
    <FormikWithRef
      validateOnChange={true}
      validateOnBlur={true}
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
      ref={formRef}
    >
      {(props) => <form onSubmit={props.handleSubmit}>{children}</form>}
    </FormikWithRef>
  );
};

export default Form;

FormikWithRef.js

import React, { forwardRef, useImperativeHandle } from "react";
import { Formik } from "formik";

function FormikWithRef(props, ref) {
  let _formikProps = {};

  useImperativeHandle(ref, () => _formikProps);

  return (
    <Formik {...props}>
      {(formikProps) => {
        _formikProps = formikProps;
        if (typeof props.children === "function") {
          return props.children(formikProps);
        }
        return props.children;
      }}
    </Formik>
  );
}

export default forwardRef(FormikWithRef);

I have some tabs, that update an easy-peasy store state type, when I select the 2nd tab, I was wanting to update the input value (that initially comes from the store state of value) with a Formik form, but updating state initialValues specific to that component that gets passed as initialValues prop to the Formik component.

TabsForm.js

import React, { useState, useEffect, useRef } from "react";
import styled from "styled-components";
import { useStoreState } from "easy-peasy";
import Form from "./Form";
import MoneyBox from "./MoneyBox";

const Container = styled.div`
  width: 100%;
  background-color: #dfdfdf;
`;

const FieldWrapper = styled.div`
  padding: 20px 12px;
`;

const TabsForm = () => {
  const [initialValues, setInitialValues] = useState();

  const type = useStoreState((state) => state.type);
  const value = useStoreState((state) => state.value);

  const formRef = useRef(null);

  const onFormSubmit = async (values) => {
    console.log({ values });
  };

  useEffect(() => {
    if (value && type) {
      let filterVal = { ...value };
      /*  here is where I update the input value to be 3000, 
      the initial values get updated and in the `Form.js` file, 
      the console log from here also reflects this update,
      however, the input field does not update? */
      if (type === "Two") filterVal.input = 30000;
      setInitialValues(filterVal);
    }
  }, [value, type]);

  useEffect(() => {
    //   check initialValues has updated
    console.log({ initialValues });
  }, [initialValues]);

  return (
    <Container>
      {initialValues && type ? (
        <Form
          initialValues={initialValues}
          onSubmit={onFormSubmit}
          formRef={formRef}
        >
          <FieldWrapper>
            <MoneyBox name="input" currencySymbol={"£"} />
          </FieldWrapper>
        </Form>
      ) : null}
    </Container>
  );
};

export default TabsForm;

When clicking the 2nd tab;

  • The initialValues state in TabsForms.js updates so that value.input = 30000;
  • The initialValues prop in both Form.js and FormikWithRef.js also reflect that value.input = 3000
  • However, the input does not update, using the useField hook from forimk in the MoneyBox.js component, the field object does not have a value of 30000, instead it's whatever the field value was before, why is this?

I have created a CodeSandbox to see all the components used, and console logs to see that the Formik does receive the updated value, but doesn't seem to apply it.

I've been stuck on this for a few days and can't seem to find a solution, any help would be greatly appreciated.

3

3 Answers

10
votes

If you want the value of the input to change when you change initialValues, you need to pass to the Formik component the prop enableReinitialize as true.

So, what you need to change in your code is in TabsForm.js pass to your Form component the prop enableReinitialize

<Form
  enableReinitialize
  initialValues={initialValues}
  onSubmit={onFormSubmit}
  formRef={formRef}
>
  <FieldWrapper>
    <MoneyBox name="input" currencySymbol={"£"} />
  </FieldWrapper>
</Form>

And in your Form.js pass that prop to the Formik component

const Form = ({
  formRef,
  children,
  initialValues,
  validationSchema,
  onSubmit,
  enableReinitialize
}) => {
  return (
    <FormikWithRef
      enableReinitialize={enableReinitialize}
      validateOnChange={true}
      validateOnBlur={true}
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
      ref={formRef}
    >
      {(props) => <form onSubmit={props.handleSubmit}>{children}</form>}
    </FormikWithRef>
  );
};

I'm not quite sure how you business logic should work, but here is a working example with the changes above.

0
votes

( Solution CodeSandbox)

It's called initialValues, so why you expect it to update the form values when you change it? (however you can ask it to do so by using enableReinitialize prop, as @Vencovsky mentioned in another answer.)

For binding your desired value (value.input in the easy-peasy store) to formik input, you can use:

const [field, meta, helpers] = useField(props);
useEffect(() => {
  helpers.setValue(value.input)
}, [value])

it will update the value of the formik input field every time the value in store changes.

and for changing the value of the state in store, you can use the way you did it for setting tabs. (using easy-peasy store.)

Run It On CodeSandbox

On line 49 in Tabs.js, it updates the value when a tab is clicked.

On line 19 in Input.js, it binds the input value to your store state.

-3
votes

enableReinitialize={true} prop will do the trick for you