Search code examples
reactjstypescriptformikreact-typescript

formik with typescript set initalvalue in withFormik HOC


I am adding formik with typescript and react to my project, so i am using withFormik hook as an HOC, the issue i am facing is that i am not able to set initialvalue (defined in hoc) to the response i am getting from the api

login.tsx

import { FormikProps } from "formik";
export interface FormValues {
  title: string;
}

export const Login: React.FC<FormikProps<FormValues>> = (props) => {
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/todos/1")
      .then((response) => response.json())
      .then((json) => setTitle(json.title));
  }, []);
  const [title, setTitle] = useState<string | null>(null);


  const { handleSubmit, handleBlur, handleChange, touched, errors, values } =
    props;

  return (
    <Container component="main" maxWidth="xs">

      <div className={classes.paper}>


        <form className={classes.form} noValidate onSubmit={handleSubmit}>
          <TextField
            variant="outlined"
            margin="normal"
            required
            fullWidth
            id="title"
            label="Title"
            name="title"
            value={values.title}
            autoFocus
            autoComplete="off"
            onChange={handleChange}
            onBlur={handleBlur}
          />

         

          <Button
            type="submit"
            fullWidth
            variant="contained"
            color="primary"
            className={classes.submit}
          >
            Sign In
          </Button>
        </form>
      </div>
    </Container>
  );
};

login.hoc.tsx

import { Form, FormikProps, FormikValues, useFormik, withFormik } from "formik";


import { FormValues, Login } from "./index";
interface MyFormikProps {

}

export const LoginView = withFormik<MyFormikProps, FormValues>({
  enableReinitialize: true,
  mapPropsToValues: (props) => {
    return { title:"" };
  },

  handleSubmit: (values) => {
    console.log(values);
  },
})(Login);

This works perfectly fine, but my issue is that suppose i hit an api in didmount of login.tsx and then you can see i set the "title" to response what i am getting from api

Now I want to set initialValue of "title" to what i am getting as response from api


Solution

  • Why Your HOC Won't Work

    Your current setup cannot possibly work because the hierarchy is incorrect. The withFormik HOC creates the initial value from the props of the component. But the API response is never in the props of Login. It's saved to a state inside of the Login component. An HOC, which is outside, cannot access it.

    You could have a setup where withFormik gets the correct API data in props, but you would need to move the API fetching up to a higher level.

    // component which renders the form, no more API fetch
    export const Login: React.FC<FormikProps<FormValues>> = (props) => {/*...*/}
    
    // data passed down from API
    interface MyFormikProps {
      title: string | null;
    }
    
    // component which sets up the form
    export const LoginView = withFormik<MyFormikProps, FormValues>({
      enableReinitialize: true,
      mapPropsToValues: (props) => ({
        title: props.title ?? "" // fallback if null
      }),
      handleSubmit: (values) => {
        console.log(values);
      }
    })(Login);
    
    // component which loads the data
    export const ApiLoginForm = () => {
      const [title, setTitle] = useState<string | null>(null);
    
      useEffect(() => {
        fetch("https://jsonplaceholder.typicode.com/todos/1")
          .then((response) => response.json())
          .then((json) => setTitle(json.title));
      }, []);
    
      return <LoginView title={title} />;
    };
    

    Code Sandbox Link

    Handling Async Initial Values

    The value from your API will take time to resolve. It's tricky to use it as an initial value because the initial value must be present as soon as the form is mounted. What we are doing here is falling back to using an empty string '' as an initial value. Then once the API response loads, the form resets with the new initial value because you've used the enableReinitialize setting.

    This will reset the whole form, and could cause a bad user experience if the API response is slow. You wouldn't want to override user inputs if the user entered text in form before the response came back. You could disable the field until the response is available. Or you could make the modification conditional by checking if touched.title is true or if values.title. If you want to get more sophisticated about how you modify the form, then read on...

    Programmatically Modifying Formik Values

    You can make use of the setFieldValue prop from FormikProps to imperatively modify any value of your field. Instead of changing the initial value, you can change the current value by calling setFieldValue.

    This approach is more in line with your current setup as it allows you to keep the API calls inside of Login. We don't need the local title state. Instead of calling setTitle with the resolved response, you can call setFieldValue.

    export const Login: React.FC<FormikProps<FormValues>> = (props) => {
    
      // access setFieldValue from props
      const { setFieldValue, handleSubmit, handleBlur, handleChange, touched, errors, values } =
        props;
    
      useEffect(() => {
        fetch("https://jsonplaceholder.typicode.com/todos/1")
          .then((response) => response.json())
          // could make this conditional, or add extra logic here
          .then((json) => setFieldValue("title", json.title));
      }, []);
    
      return (
        /* same JSX as before */
      )
    };
    

    Code Sandbox Link