Pijus Serapinas
Pijus Serapinas

Reputation: 1

useFormContext updates the input values, but not the state of the form

useFormContext changes the value of the input and the state of the request payload sent to API.

import { FC, useEffect, useRef, useState } from "react";
import {
  ArrayInput,
  BooleanInput,
  Create,
  NumberInput,
  SelectInput,
  SimpleFormIterator,
  TextInput,
  required,
  AutocompleteArrayInput,
  ImageInput,
  useRedirect,
  Form,
  SimpleForm,
} from "react-admin";
import Box from "@mui/material/Box";
import { baseUrl } from "../dataProvider";
import { useFormContext } from "react-hook-form";
import { useOutsideAlerter } from "../hooks/useOutsideClick";

const initValue = {
  index: null,
  position: [0, 0],
};

const CreateForm: FC<{ ingredientChoices: { id: string; name: string }[] }> = ({
  ingredientChoices,
}) => {
  const [availableIngredients, setAvailableIngredients] = useState<
    Record<number, string>
  >({});
  const [nameValue, setNameValue] = useState<{ name: string; index: number }>({
    name: "",
    index: 0,
  });
  const [open, setOpen] = useState<{
    index: number | null;
    position: number[];
  }>(initValue);
  const { setValue } = useFormContext();
  const outside = useRef(null);
  useOutsideAlerter(outside, () => setOpen(initValue));

  const getChoices = () => {
    return Object.values(availableIngredients).map((value) => ({
      id: value,
      name: value,
    }));
  };

  const fetchAttributes = (id: string) => {
    fetch(`${baseUrl}/bo/ingredients/${id}`)
      .then((res) => res.json())
      .then((data) => {
        Object.entries(data).forEach(([key, value]) => {
          if (
            [
              "units",
              "value",
              "kcal",
              "protein",
              "carbs",
              "fat",
              "allergies",
            ].includes(key)
          ) {
            setValue(`ingredients.${nameValue.index}`, { [key]: value });
          }
        });
      });
  };

  return (
    <>
      {open.index === nameValue.index && (
        <Box
          ref={outside}
          boxShadow={2}
          sx={{
            position: "absolute",
            maxHeight: "250px",
            overflowY: "scroll",
            overflowX: "hidden",
            top: open.position[1] + 500,
            left: open.position[0],
            borderRadius: "10px",
            backgroundColor: "white",
            zIndex: 1000,
          }}
        >
          {ingredientChoices
            .filter(({ name }) =>
              nameValue.name ? name.includes(nameValue.name) : true,
            )
            .map(({ id, name }) => (
              <Box
                p={1}
                key={id}
                sx={{
                  cursor: "pointer",
                }}
                onClick={() => {
                  setValue(`ingredients.${nameValue.index}`, { name });
                  setOpen(initValue);
                  fetchAttributes(id);
                }}
              >
                {name}
              </Box>
            ))}
        </Box>
      )}
      <ArrayInput source="ingredients" validate={[required()]}>
        <SimpleFormIterator inline>
          <TextInput
            onChange={(e) => {
              const rect = e.target.getBoundingClientRect();
              setAvailableIngredients({
                ...availableIngredients,
                [e.target.id.split(".")[1]]: e.target.value,
              });
              setNameValue({
                name: e.target.value,
                index: +e.target.id.split(".")[1],
              });
              setOpen({
                index: +e.target.id.split(".")[1],
                position: [rect.left, rect.top],
              });
            }}
            source="name"
            validate={[required()]}
            helperText={false}
          />
          <TextInput
            validate={[required()]}
            source="units"
            helperText={false}
          />
          <NumberInput
            validate={[required()]}
            source="value"
            helperText={false}
          />
          <NumberInput
            validate={[required()]}
            source="kcal"
            helperText={false}
          />
          <NumberInput
            validate={[required()]}
            source="protein"
            helperText={false}
          />
          <NumberInput
            validate={[required()]}
            source="carbs"
            helperText={false}
          />
          <NumberInput
            validate={[required()]}
            source="fat"
            helperText={false}
          />
          <SelectInput choices={getChoices()} source="linkedIngredient" />
          <AutocompleteArrayInput
            fullWidth
            choices={allergenChoices}
            source="allergies"
          />
        </SimpleFormIterator>
      </ArrayInput>
    </>
  );
};

const RecipeCreate = () => {
  const redirect = useRedirect();
  const [ingredientChoices, setIngredientChoices] = useState<
    {
      id: string;
      name: string;
    }[]
  >([]);

  useEffect(() => {
    fetch(`${baseUrl}/bo/ingredients`)
      .then((res) => res.json())
      .then((data) => {
        setIngredientChoices(data);
      });
  }, []);

  return (
    <Create
      mutationOptions={{
        onSuccess: () => {
          redirect("/");
        },
      }}
    >
      <SimpleForm>
        <CreateForm ingredientChoices={ingredientChoices} />
      </SimpleForm>
    </Create>
  );
};

export default RecipeCreate;

This should basically create a dropdown of all the available ingredients in the databse and when one is clicked, the data in the form should prefill... But now I'm getting this weird case when I submit the prefilled form.

Screenshot from backoffice create form

I tried changing out the form provider library, none of them worked... Also tried moving the form itself out into a separate component, to see if the problem was the Form component

Upvotes: 0

Views: 23

Answers (1)

slax57
slax57

Reputation: 593

Not 100% sure but you may be in one of the edge cases where

It's recommended to target the field's name rather than make the second argument a nested object.

(taken from react-hook-form's docs)

Can you try to apply that rule? For instance:

- setValue(`ingredients.${nameValue.index}`, { [key]: value });
+ setValue(`ingredients.${nameValue.index}.${key}`, value);

Upvotes: 0

Related Questions