How to Formik setFieldValue in useEffect hook

Viewed 26455

I have a Formik form that needs to dynamically change based on information passed via the router. I need to run a graphQL query to retrieve some data and populate the form with that retrieved data. I am able to set up the form and retrieve the data but I can't figure out how to setFieldValue for the underlying form while in a useEffect hook. I feel like I'm missing some important piece to get access to the Formik context but I haven't been able to figure it from the docs.

Any help would be great.

import React, { useState, useEffect } from "react";
import Router, { useRouter } from "next/router";

import Container from "react-bootstrap/Container";
import { Field, Form, FormikProps, Formik } from "formik";
import * as Yup from "yup";

import { useLazyQuery } from "@apollo/react-hooks";
import { GET_PLATFORM } from "../graphql/platforms";

export default function platformsForm(props) {
  const router = useRouter();

  // grab the action requested by caller and the item to be updated (if applicable)
  const [formAction, setFormAction] = useState(router.query.action);
  const [formUpdateId, setFormUpdateId] = useState(router.query.id);

  const [initialValues, setInitialValues] = useState({
    platformName: "",
    platformCategory: ""
  });

  const validSchema = Yup.object({
    platformName: Yup.string().required("Name is required"),
    platformCategory: Yup.string().required("Category is required")
  });

  const [
    getPlatformQuery,
    { loading, error, data: dataGet, refetch, called }
  ] = useLazyQuery(GET_PLATFORM, {
    variables: { id: formUpdateId }
  });

  useEffect(() => {
    !called && getPlatformQuery({ variables: { id: formUpdateId } });
    if (dataGet && dataGet.Platform.platformName) {
      console.log(
        dataGet.Platform.platformName,
        dataGet.Platform.platformCategory
      );

      //
      // vvv How do I set Field values at this point if I don't have Formik context
      // setFieldValue();
      //
    }
  }),
    [];

  const onSubmit = async (values, { setSubmitting, resetForm }) => {
    console.log("submitted");
    resetForm();
    setSubmitting(false);
  };

  return (
    <Container>
      <Formik
        initialValues={initialValues}
        validationSchema={validSchema}
        onSubmit={onSubmit}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          handleReset,
          values,
          touched,
          isInvalid,
          isSubmitting,
          isValidating,
          submitCount,
          errors
        }) => (
          <Form>
            <label htmlFor="platformName">Name</label>
            <Field name="platformName" type="text" />

            <label htmlFor="platformCategory">Category</label>
            <Field name="platformCategory" type="text" />

            <button type="submit">Submit</button>
          </Form>
        )}
      </Formik>
    </Container>
  );
}

7 Answers

I think I figured this out but am not sure. I found a some places that referred to aa Formik innerRef prop so tried that and it seems to work. It's not mentioned anywhere that I could see in either the doc or in the tutorial so I'm not sure if this is some function that is unsupported, or maybe just supposed to be used for internal Formik stuff but it seemed to work for me so I'm going to use that until I find a better way. I've already spent way longer on this that I care to share. :|

Comments or suggestions are welcome. Or maybe upvotes if you think this is the right approach.

To resolve this I add a useRef in the function main body:

  const formikRef = useRef();

Then I added that as a prop:

      <Formik
        innerRef={formikRef}
        initialValues={initialValues}
        validationSchema={validSchema}
        onSubmit={onSubmit}
      >

Once I did that I was able to reference the Formik functions from within useEffect so in my case I did the following:

      if (formikRef.current) {
        formikRef.current.setFieldValue(
          "platformName",
          dataGet.Platform.platformName
        );
        formikRef.current.setFieldValue(
          "platformCategory",
          dataGet.Platform.platformCategory
        );
      }

The right way to access Formik state and helpers is to use the Formik's useFormikContext hook. This gives you everything you need.

Check the docs for details and an example: https://formik.org/docs/api/useFormikContext

I ran into a similar problem few days ago, I wanted to reuse a form for both creating and updating event. While updating, I'm fetching the data of an already existing event from the DB and populating each field with the corresponding value.

I was able to use useEffect, and setFieldValue inside formik by changing the arrow function to a named function like this

    <Formik
  initialValues={initialValues}
  validationSchema={validationSchema}
  onSubmit={handleSubmit}
>
  {function ShowForm({
    values,
    errors,
    touched,
    handleChange,
    handleSubmit,
    handleBlur,
    setFieldValue,
    isValid,
    dirty,
  }) {
    useEffect(() => {
      if (!isCreateMode) {
        axios
          .get(`/event/${id}`)
          .then((response) => {
            const data = response.data.data;
            const fields = [
              "title",
              "description",
              "startDate",
              "endDate",
              "time",
              "venue",
              "link",
              "banner",
            ];
            fields.forEach((field) => {
              setFieldValue(field, data[field], false);
            });
          })
          .catch((error) => console.log("error:", error));
      }
    }, [setFieldValue]);
    return (
      <form action="" onSubmit={handleSubmit} className="event-form">
        <Grid container justify="center">
          <Grid item lg={12} style={{ textAlign: "center" }}>
            <h2>{isCreateMode ? "Create New Event" : "Edit Event"}</h2>
          </Grid>
          <Grid item lg={6}>
            <TextField
              name="title"
              id="title"
              label="Event title"
              value={values.title}
              type="text"
              onBlur={handleBlur}
              error={touched.title && Boolean(errors.title)}
              helperText={touched.title ? errors.title : null}
              variant="outlined"
              placeholder="Enter event title"
              onChange={handleChange}
              fullWidth
              margin="normal"
            />
            <Button
              variant="contained"
              disableElevation
              fullWidth
              type="submit"
              disabled={!(isValid && dirty)}
              className="event-submit-btn"
            >
              Publish Event
            </Button>
          </Grid>
        </Grid>
      </form>
    );
  }}
</Formik>;

Coming back to your use case you can simply refactor your form to this

return (
  <Container>
    <Formik
      initialValues={initialValues}
      validationSchema={validSchema}
      onSubmit={onSubmit}
    >
      {function myForm({
        handleSubmit,
        handleChange,
        handleBlur,
        handleReset,
        values,
        touched,
        isInvalid,
        isSubmitting,
        isValidating,
        submitCount,
        errors,
      }) {
        useEffect(() => {
          !called && getPlatformQuery({ variables: { id: formUpdateId } });
          if (dataGet && dataGet.Platform.platformName) {
            console.log(
              dataGet.Platform.platformName,
              dataGet.Platform.platformCategory
            );

            {/* run setFieldValue here */}
          }
        }, []);
        return (
          <Form>
            <label htmlFor="platformName">Name</label>
            <Field name="platformName" type="text" />

            <label htmlFor="platformCategory">Category</label>
            <Field name="platformCategory" type="text" />

            <button type="submit">Submit</button>
          </Form>
        );
      }}
    </Formik>
  </Container>
);

One trick to get around this mess is to have an invisible button inside a Formik form. This button's onClick will have access to everything Formik-related, such as setFieldValue, setTouched, etc. Then you can "simulate" the click of this button from useEffect using document.getElementById('..').click(). That will allow you to perform Formik actions from a useEffect.

E.g.

// Style it to be invisible
<Button id="testButton" type="button" onClick={() => {
    setFieldValue('test', '123');
    setTouched({});    
    // etc. any Formik action
}}>
</Button> 

useEffect:

useEffect(() => {
    document.getElementById("testButton").click(); // Simulate click
}, [someVar);

Just was dealing with this issue for a while and found the following solution leveraging useFormikContext inside a nested component.

// this is a component nested within a Formik Form, so it has FormikContext higher up in the dependency tree

const [field, _meta, helpers] = useField(props);
const { setFieldValue } = useFormikContext();
const [dynamicValue, setDynamicValue] = useState('testvalue')

const values = ['value1', 'value2', dynamicValue];

const [selectedIndex, setSelectedIndex] = useState(field.value);

// have some update operation on setting dynamicValue

//handle selection 
const handleSelect = (index) => { 
setSelectedIndex(index);
 helpers.setField(values[index])
}



//handle the update
  useEffect(() => {
    setCustomTheme(dynamicValue);
    setFieldValue("<insertField>", dynamicValue);
  }, [dynamicValue, setFieldValue]);

This requires a parent Formik element so it can leverage the useField and useFormikContext correctly. It appears to update correctly and is running for us.

I solved a similar issue by using the useFormikContext hook. This makes the setFieldValue method available, which you can use within useEffect. I had to add this logic in a separate component and pass this into the form in order to be within the formik context.

This part of the documentation should help: https://formik.org/docs/api/useFormikContext

Related