Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


37. Add Update Recipe Form

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.

components/UpdateRecipe.tsx

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.

utils/submitForm.ts

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.

pages/my-recipes/[id].tsx

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.