Photo by Ciprian Boiciuc on Unsplash

Easy and readable React/GraphQL/Node Stack — Part 4

Zac Tolley
Scropt
Published in
11 min readMay 5, 2019

--

So far we have created an application that runs an Apollo GraphQL server that serves a React JS application equipped with hooks generated to use Apollo GraphQL client and a little bit of Typescript to help with autocomplete. In this part of the series we’ll introduce forms. We could have used a plain form with our own event handlers set on the fields but this doesn’t scale once you start to introduce validation or componentised fields, for this reason we’ll use a form library. The code for this section is available on Github.

Formik

The two leading solutions in React JS for forms are Redux Forms and Formik. With the GraphQL stack the need for application wide state is diminished and if we wanted to use Redux Forms it would end up being the only user of Redux. Formik has grown in popularity and provides comprehensive solution without the need for Redux.

To help guide things we’ll take a look at what we will aim to render by the end of this tutorial.

We need to create a form with a textfield, dropdown and a checkbox. Note that the form fields have labels and are styled. For this project the styles will come from Bootstrap so go ahead and add a tag to your page head to pull in the required CSS.

<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"crossorigin="anonymous"/>

We could manually do all the markup for the components in the form component but that wouldn’t make the fields very re-usable so each of these form fields will be built as a component.

Form Components

Let’s start by creating the components we’ll need for our form, the easiest being the text input.

# client/src/components/Input/index.tsximport { Field, ErrorMessage } from 'formik'
import React from 'react'
interface InputProps {
disabled?: boolean
footnote?: string
label?: string
name: string
type?: string
}
const Input = (props: InputProps): JSX.Element => {
const {
disabled = false,
footnote,
label,
name,
type = 'text'
} = props
return (
<div className="form-group">
{label && <label htmlFor={name}>{label}</label>}
<Field
className="form-control"
disabled={disabled}
name={name}
type={type}
/>
<small className="form-text text-muted">{footnote}</small> <ErrorMessage
name={name}
className="invalid-feedback"
component="div"
/>
</div>
)
}
export default Input

Formik manages forms, their lifecycle and also manages fields including their state, errors, validation and rendering.

The purpose of this Input component is to render a text input with an associated label, any errors and apply the Bootstrap CSS classes. The component at the very least needs to know the name of the form field it is rendering. If no type is provided it then assumes this is a text field and it is not disabled. The ‘InputProps’ interface takes the place of PropTypes in Javascript React and provides a type definition for the props parameter object that the function expects. Types with ? in their definition are optional. Default prop values are defined like regular defaults in the function signature. The component defines the markup for the Input component, optionally displaying a label. The Field and Error components are part of Formik. Formik is responsible for rendering the field itself, though if you require custom markup a component can be passed in that is responsible for rendering the field with a value and event handlers passed to it. The Input component just needs a regular input so it calls the Field component with the properties required, including an optional className for the input. Error message will look for any errors for the field by name and render them using the className and tag type specified.

The checkbox is near identical.

# client/src/components/Checkbox/index.tsximport { Field, ErrorMessage } from 'formik'
import React from 'react'
interface CheckboxProps {
disabled?: boolean
footnote?: string
label?: string
name: string
type?: string
}
const Checkbox = (props: CheckboxProps): JSX.Element => {
const {
disabled = false,
footnote,
label,
name
} = props
return (
<div className="form-group">
{label && <label htmlFor={name}>{label}</label>}
<Field
className="form-control"
disabled={disabled}
name={name}
type="checkbox"
/>
<small className="form-text text-muted">{footnote}</small>

<ErrorMessage
name={name}
className="invalid-feedback"
component="div"
/>
</div>
)
}
export default Checkbox

Select is a little different, thankfully Formik is aware of selects and allows you to pass through options as child elements.

# client/src/components/Select/index.tsximport { Field, ErrorMessage } from 'formik'
import React from 'react'
export interface Option {
label: string
value: string
}
export interface SelectProps {
className?: string
disabled?: boolean
footnote?: string
label?: string
name: string
options: Option[]
}
const Select = (props: SelectProps): JSX.Element => {
const {
disabled = false,
footnote,
label,
name,
options
} = props
return (
<div className="form-group">
{label && <label htmlFor={name}>{label}</label>}
<Field
name={name}
className="form-control"
component="select"
disabled={disabled}
>
{options.map(({ label, value }) => (
<option key={value} value={value}>{label}</option>
))}
</Field>
<small className="form-text text-muted">{footnote}</small> <ErrorMessage
name={name}
className="invalid-feedback"
component="div"
/>
</div>
)
}
export default Select

The only difference between this component and others is that a select requires options which can be passed in as children. Formik knows which one is selected.

Now we have the form elements we can construct the form. The form code will be split into 2 halves, one half of the form code will concentrate on specifying the form layout and the fields it includes and the other half will be responsible for providing data to the form and handling events such as form submission.

Form Layout

Starting with the form layout we want to define a form with 3 fields and buttons for submission and cancel. Note that the dropdown to select a project is populated with a list provided from the server

# client/src/forms/Todo/index.tsximport { Formik, Form } from 'formik'
import React from 'react'
import { Checkbox, Input, Select } from '../../components'
import { Option } from '../../components/Select'
import { Project, Todo, useProjectListQuery } from '../../graphql'
interface TodoFormProps {
todo: Todo
onCancel: () => void
onSubmit: (formData: Todo) => void
}
const projectToOption = ({ id, title }: Project): Option => ({
value: id,
label: title,
})
const TodoForm = (props: TodoFormProps): JSX.Element => {
const { todo, onCancel, onSubmit } = props
const { data, error, loading } = useProjectListQuery()
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
const projectOptions = data.projects.map(projectToOption)
return (
<Formik initialValues={todo} onSubmit={onSubmit}>
<Form>
<Input type="text" name="title" label="Title" />
<Select
name="project.id"
label="Project"
options={projectOptions}
/>
<Checkbox name="complete" label="Complete?" /> <hr /> <button className="btn" type="button" onClick={onCancel}>
Cancel
</button>
<button className="btn btn-primary" type="submit">
Save
</button>
</Form>
</Formik>
)
}
export default TodoForm

It may look like a-lot but if you read through you’ll see it’s relatively simple. We define the property types using Typescript, there’s a helper function to generate options in the correct format and we use a graphQL hook to go get a list of projects to populate the dropdown.

The Formik tag sets up a context to co-ordinate form state, field rendering and handle the submission and validation process. In this case we only need to tell Formik the initial values to populate the form and a handler for submission. The default properties can be any object and are mapped to fields using their ‘name’ property. Name property names can include nested data as you can see in the project field. When the form is posted we’ll flatten this but you could use Input types in your GraphQL schema. The fact that Formik uses a Context to store it’s state means that we don’t need an application wide state library and yet our child components have access to the form without having to manually pass everything through props.

There is an important anomaly to watch out for if you want to use nested objects for mutations. When Apollo fetches objects from the server it also gets/populates a ‘__typename’ property. When we populate the form with the ‘todo’ it also includes the ‘__typename’ property in the form state, it also includes it in the nested project property too. If we generate a form and then submit it formik will pass an object to the onSubmit handler that still has those ‘__typename’ properties. If our code then calls a mutation with the form data including the nested project flagged as ‘__typename: “project”’ then the server will throw an error saying the type is wrong as it would want a ‘projec input type’. If you want to use nested objects in forms and pass them to mutations you must strip the ‘__typename’ properties from the object you pass to Formik or from the object passed to your call to the mutation. I recommend having your onSubmit build a new object by picking the properties it needs in order to call the mutation with sanitised data.

Back to the form, we use a Form component to act as a form tag and make use of the form input components we created earlier, this results semantic markup. The reason that we define a form component to concentrate purely on rendering is so that we can re-use the form in multiple places, in our case we can use the same form for editing a todo as we would use for adding a new one.

Pages and Fragments

In the stack we are building there are two terms I use above the level of components and forms: ‘pages’ and ‘fragments’. A ‘fragment’ is a piece of a page that is made up of multiple elements (forms and components) combined. Pages are a level above fragments. You may have a complex web page that has multiple fragments such as one fragment for listing products and a second displaying the contents of a shopping basket. In simple pages, like the edit todo page there would only be a single fragment so we will just create a page.

Form data and event handlers

The todo edit page will display a title, form and will be responsible for handling form submission, cancellation and navigation. In some situations with more complexity you may break out the form handling element to its own component.

import React from 'react'
import { RouteComponentProps } from 'react-router-dom'
import {
Todo,
UpdateTodoMutationVariables,
useToDoQuery,
useUpdateTodoMutation,
} from '../../graphql'
import TodoForm from '../../forms/Todo'
interface TodoParams {
id: string
}
const TodoEdit = (props: RouteComponentProps<TodoParams>):
JSX.Element => {

const {
history,
match: {
params: { id },
},
} = props
const updateTodoMutation = useUpdateTodoMutation()
const {
data, error, loading
} = useToDoQuery({ variables: { id }})
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
const { todo } = data

const onSubmit = async (formData: Todo): Promise<void> => {
const variables: UpdateTodoMutationVariables = {
id: formData.id,
title: formData.title,
complete: formData.complete,
projectId: formData.project.id,
}
await updateTodoMutation({ variables })
history.push('/')
}
const onCancel = () => {
history.push('/')
}
return (
<div className="container">
<h1>Edit Todo</h1>
<TodoForm
todo={todo}
onSubmit={onSubmit}
onCancel={onCancel}
/>
</div>
)
}
export default TodoEdit

The page we are using is called by React Router which means (after some investigation) that it will be called with a number of properties including a history object for navigation and a match property detailing why this page was picked and associated parameters. For the type definition for the Route Properties we can define a type that says ‘This parameter is of type RouteComponentProps and has parameters that comply with the TodoParams interface’. For the Java devs in the room I’m sure you understand this, JS devs it’s ok if you don’t, just admire that it gives you autocomplete for all the router properties and will know that you have an id parameter.

Next there a couple of hooks we need to get, one to mutate data and another to fetch data. The mutation hook is called with no parameters but does accept some if required. GraphQL maintains a copy of all our data in a local cache and in some situations the action we carry out may cause the local cache to be out of date with the server. If we created a new project here then the project list could potentially be out of date. The mutation hook accepts a parameter to specify queries that should be re-run to update the local cache after the mutation, amongst other things. The documentation for Apollo GraphQL client provides details of possible configuration options that can be used with mutation hooks.

The submission handler sanitises and flattens the form data, see earlier comments, then calls the mutation like a regular function. Note the ‘await’, we the mutation hook provided uses promises and once it is done it either returns the data returned from the mutation or an error using the state syntax as the query hook ‘{data, error}’. For now we trust it works and send the user back to the home page.

More queries

In the earlier articles we creates some client GraphQL queries to get a list of todos, we need a few more now to get an individual todo, get a list of projects and to update the todo. The query GraphQL should be simple to follow, the mutation is similar to one described earlier.

# client/src/graphql/queries/Todo.graphqlquery ToDo($id: ID!) {
todo(id: $id) {
id
title
complete
project {
id
title
}
}
}

# client/src/graphql/queries/ProjectList.graphql
query ProjectList {
projects {
id
title
}
}

# client/src/graphql/mutations/UpdateTodo.graphql
mutation UpdateTodo(
$id: ID!
$title: String!
$projectId: String
$complete: Boolean!
) {
updateTodo(
id: $id,
title: $title,
projectId: $projectId,
complete: $complete
) {
id
title
project {
id
title
}
complete
}
}

Add edit page to app

If you are running your dev server these queries will get analysed and the hooks and type definitions will be auto-generated. All that is left to do is wire the edit page up to the application.

# client/src/pages/Home/index.tsx...
import { Link } from 'react-router-dom'
...
<div className="container">
<h1>Todos</h1>
<ul>
{todos.map(({ title, id }) => (
<li key={id}>
<Link to={`/edit/${id}`}>{title}</Link>
</li>
))}
</ul>
</div>
...

# client/src/App.tsx
import { Route, Switch } from 'react-router-dom'
import Home from './pages/Home'
import TodoEdit from './pages/TodoEdit'
const App = () => (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/edit/:id" component={TodoEdit} />
</Switch>
)

You can now fire up your application, view a list of todos and edit one then save it. If you wish to add validation then I recommend looking at ‘Yup’, a validation library that works very well with Formik. Yup allows you to define your validation logic, error messages and can be attached to your Formik form via a single parameter. Using this tool your data is validated and checked before reaching the onSubmit call.

Summary

Admittedly this is a trivial example of how to apply this stack and there is more that is required with regards to subjects such as validation, error handling, styling, linting and testing. I hope this demonstrates how Javascript, GraphQL, Typescript and a good ORM can combine to be greater than their individual parts and provides an easy and productive stack. The Javascript landscape being what it is thing change constantly and there is nothing to say that next week Facebook won’t come up with an amazing new version of Relay or it may turn out that Prismas ORM combined with Typescript has big advantages on the server side. During the course of writing these articles the package name for the code generator tool changed and went through a major version change. We are still stuck with the fact that when we re-visit our code in 6 months all of its dependencies may be viewed as ancient.

As always the code is available on Github via this tag and you may find there are new versions over time as I add linting and testing. If you have any general comments then post them here. If you have bugs post them on the Github project and if you want to say hi then I’m on Twitter as ‘@ztolley’.

--

--