Archived course lesson

Add Update Recipe Form

Part of Frontend Serverless with React and GraphQL

Objective: Create a UpdateRecipe form to allow users to edit their existing recipes.

We need to first create an update recipe component which will do all of the heavy lifting of creating the form for us. It will be almost an identical copy of the create recipe form that we’ve already made, but it also has an additional feature that it will fetch the current recipe and update all the fields with the current data. Since this is an asyncronous process, we can’t populate those fields as defaults for our submitForm hook like we’d ideally like to but will instead have to directly set them once the get recipe query data has been loaded. We use a new state isQueryLoading to make sure that we only populate the fields the first time that we get the data and not every time that the form re-renders.

import { useQuery } from '@apollo/react-hooks';
import { recipeGraphQL } from '../graphql/queries/recipe';
import { submitForm } from '../utils/submitForm';
import { useState } from 'react';
import * as _ from 'lodash';
import { Form, Row, Col, Button } from 'antd';
import { GenerateInput, GenerateTextInput } from './GenerateFields';
import { GenerateIngredients } from './GenerateIngredients';
import { Loading } from './notify/Loading';

export const UpdateRecipe = ({ id }) => {
  const { loading: isQueryLoading, data, error } = useQuery(recipeGraphQL, {
    variables: { where: { id } },
  });

  const [recipeState, setRecipeState] = useState({ isQueryLoading: true });

  const initiateUpdateRecipe = () => console.log('updated');

  const {
    inputs,
    handleInputChange,
    handleAddIngredient,
    handleDeleteIngredient,
    handleDropdownChange,
    handleSubmit,
    setInputs,
  } = submitForm(
    {
      title: '',
      description: '',
      content: '',
      ingredients: [],
    },
    initiateUpdateRecipe,
  );
  if (!isQueryLoading && recipeState.isQueryLoading) {
    const { __type, ...loadedRecipe } = _.get(data, 'recipe');
    setInputs((state) => ({ ...state, ...loadedRecipe }));
    setRecipeState((state) => ({ ...state, isQueryLoading }));
  }

  if (!data) return <Loading />;

  return (
    <Form>
      <GenerateInput
        name="title"
        value={inputs.title}
        handleInputChange={handleInputChange}
      />
      <GenerateInput
        name="description"
        value={inputs.description}
        handleInputChange={handleInputChange}
      />
      <GenerateTextInput
        name="content"
        value={inputs.content}
        handleInputChange={handleInputChange}
      />
      <GenerateIngredients
        names={['amount', 'unit', 'type']}
        values={inputs.ingredients}
        handleAddIngredient={handleAddIngredient}
        handleDeleteIngredient={handleDeleteIngredient}
        handleInputChange={handleInputChange}
        handleDropdownChange={handleDropdownChange}
      />
      <Row>
        <Col span={16} />
        <Col span={4}>
          <Form.Item label="Update Recipe">
            <Button disabled={false} type="primary" htmlType="submit">
              Update Recipe
            </Button>
          </Form.Item>
        </Col>
      </Row>
    </Form>
  );
};

Since we will be directly setting the state of our form based on the data we get from the recipe query, we will need to return the setInputs function that we were previously just using internally in the submitForm hook. Open up the utils/submitForm.ts file and just make sure that setInputs gets returned at the end of the function.

import { useState } from 'react';
import * as _ from 'lodash';

export const submitForm = (initialValues, callback) => {
  const [inputs, setInputs] = useState(initialValues);

  const handleInputChange = (event) => {
    event.persist();
    setInputs((inputs) => {
      const newInputs = _.cloneDeep(inputs);
      _.set(newInputs, event.target.name, event.target.value);
      return newInputs;
    });
  };

  const handleDropdownChange = (event) => {
    setInputs((inputs) => {
      const newInputs = _.cloneDeep(inputs);
      _.set(newInputs, event.item.props.title, event.key);
      return newInputs;
    });
  };
  const handleAddIngredient = (event) => {
    event.persist();
    setInputs((inputs) => {
      const sortedIngredients = _.sortBy(inputs.ingredients, ['key']);

      const key =
        sortedIngredients.length > 0
          ? sortedIngredients[sortedIngredients.length - 1].key + 1
          : 0;
      return {
        ...inputs,
        ingredients: _.concat(inputs.ingredients, [
          { key, amount: '', unit: '-', type: '' },
        ]),
      };
    });
  };

  const handleDeleteIngredient = (event) => {
    event.persist();
    const position = parseInt(event.target.name);
    setInputs((inputs) => ({
      ...inputs,
      ingredients: _.filter(
        inputs.ingredients,
        (_i, index) => index !== position,
      ),
    }));
  };

  const handleSubmit = () => {
    callback();
    setInputs(() => ({ ...initialValues }));
  };

  return {
    inputs,
    setInputs,
    handleSubmit,
    handleInputChange,
    handleAddIngredient,
    handleDeleteIngredient,
    handleDropdownChange,
  };
};

Finally, we will need to use our <UpdateRecipe> component on the edit recipe page that we just made in the last step.

import { MainLayout } from '../../components/layout/MainLayout';
import { UpdateRecipe } from '../../components/UpdateRecipe';

const MyRecipe = ({ id }) => {
  return (
    <MainLayout title="Update Recipe">
      <h1>Update Recipe</h1>
      <UpdateRecipe id={id} />
    </MainLayout>
  );
};

MyRecipe.getInitialProps = ({ query }) => {
  const { id } = query;
  return { id };
};

export default MyRecipe;

We should see that although the form works, we aren’t actually saving any of our changes to our backend and everything will get reset if we do a hard-refresh of our browser. We will add the update mutation in the next step.