Photo by Ciprian Boiciuc on Unsplash

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

Zac Tolley
May 5 · 11 min read

Formik

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

Form Components

# 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
# 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
# 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

Form Layout

# 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

Pages and Fragments

Form data and event handlers

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

More queries

# 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

# 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>
)

Summary

Scropt

Building online services

Zac Tolley

Written by

Building online services

Scropt

Scropt

Building online services