Skip to content

Instantly share code, notes, and snippets.

@jdretz
Last active July 26, 2022 16:39
Show Gist options
  • Save jdretz/2c76eadcd0ea46fb0aa63a25e115f4b1 to your computer and use it in GitHub Desktop.
Save jdretz/2c76eadcd0ea46fb0aa63a25e115f4b1 to your computer and use it in GitHub Desktop.
Gatsby Function, Sendgrid, ReactJS, Typescript, Formik, Material-UI, and useReducer. These files are from a ReactJS form built with Formik that sends data to Gatsby Function that triggers email via Sendgrid API. The workflow is built with Typescript and uses a custom useReducer hook to bundle state updates.
import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby'
import sendgrid from '@sendgrid/mail'
type ContactBody = {
message: string
email: string
name: string
}
export default function handler(
req: GatsbyFunctionRequest<ContactBody>,
res: GatsbyFunctionResponse
) {
if (req.method !== `POST`) {
return res.status(405).send('')
}
try {
// Your API Key from Sendgrid
if (!process.env.SENDGRID_API_KEY) {
return res.status(500).json({
message: `Failed to set up server environment`,
})
}
sendgrid.setApiKey(process.env.SENDGRID_API_KEY)
if (req.body) {
return sendgrid
.send({
from: req.body.email,
to: `XXX`,
subject: `Message on RETZDEV from ${req.body.name}`,
text: req.body.message,
html: `<p>${req.body.message}</p>`,
})
.then(() => {
res.status(200).send('')
})
}
return res.status(400).send('')
} catch (err) {
return res.status(500).json({ message: 'There was an error', error: err })
}
}
import { EmailOutlined } from '@mui/icons-material'
import { Box, Button, Typography } from '@mui/material'
import axios from 'axios'
import { Field, FieldProps, Form, Formik } from 'formik'
import React from 'react'
import * as Yup from 'yup'
import useHttpReducer from '../../hooks/useHttpReducer'
import useMediaQueries from '../../hooks/useMediaQueries'
import RDTextField from '../RDTextField/RDTextField'
type FormValues = {
name: string
email: string
message: string
}
const ContactForm: React.FC = () => {
const initVals: FormValues = {
name: '',
email: '',
message: '',
}
const { state, dispatch } = useHttpReducer()
const { theme } = useMediaQueries()
return (
<Formik
initialValues={initVals}
validationSchema={Yup.object().shape({
email: Yup.string()
.email('Email not valid')
.required('Email is required'),
name: Yup.string().required('Required.'),
message: Yup.string().required('Required.'),
})}
onSubmit={async (values, { setSubmitting, resetForm }) => {
dispatch({
type: 'SENDING',
})
try {
await axios.post('/api/contact', values)
dispatch({
type: 'SUCCESS',
})
setSubmitting(false)
resetForm()
} catch {
dispatch({
type: 'ERROR',
})
}
}}
>
<Form
style={{
paddingTop: '32px',
maxWidth: '800px',
width: '100%',
textAlign: 'center',
}}
>
<Field name='name'>
{({
field, // { name, value, onChange, onBlur }
meta,
}: FieldProps) => (
<Box pb={2}>
<RDTextField
required
fullWidth
disabled={state.loading}
id='name'
label='Name'
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
error={meta.touched && Boolean(meta.error)}
touched={meta.touched}
errorText={meta.touched && meta.error}
/>
</Box>
)}
</Field>
<Field name='email'>
{({
field, // { name, value, onChange, onBlur }
meta,
}: FieldProps) => (
<Box pb={2}>
<RDTextField
disabled={state.loading}
required
fullWidth
id='email'
label='E-mail Address'
name={field.name}
value={field.value}
onChange={field.onChange}
touched={meta.touched}
onBlur={field.onBlur}
error={meta.touched && Boolean(meta.error)}
errorText={meta.touched && meta.error}
helperText={`We'll use this email to respond to your message.`}
/>
</Box>
)}
</Field>
<Field name='message'>
{({
field, // { name, value, onChange, onBlur }
meta,
}: FieldProps) => (
<Box pb={2}>
<RDTextField
disabled={state.loading}
fullWidth
multiline
rows={5}
maxRows={10}
id='message'
label='Message'
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
touched={meta.touched}
error={meta.touched && Boolean(meta.error)}
errorText={meta.touched && meta.error}
/>
</Box>
)}
</Field>
{state.error && (
<Typography py='16px' variant='subtitle2' color='error'>
An error occurred.
</Typography>
)}
{state.success && (
<Typography
variant='subtitle2'
py='16px'
color={theme.palette.success.main}
>
Success! You will hear from us shortly!
</Typography>
)}
<Button
disabled={state.loading}
variant='contained'
color='primary'
sx={{
width: '100%',
px: 3,
py: 1,
}}
type='submit'
endIcon={<EmailOutlined />}
>
Send Now
</Button>
</Form>
</Formik>
)
}
export default ContactForm
import { useReducer } from 'react'
type HttpReducerState = {
loading: boolean
error: boolean
success: boolean
}
/**
* Based on code from this article -> https://www.sumologic.com/blog/react-hook-typescript/
*/
type HttpReducerAction =
| { type: 'SENDING' }
| { type: 'ERROR' }
| { type: 'SUCCESS' }
const initialState = {
loading: false,
error: false,
success: false,
}
const reducer = (state: HttpReducerState, action: HttpReducerAction) => {
switch (action.type) {
case 'SENDING':
return {
...state,
loading: true,
success: false,
error: false,
}
case 'ERROR':
return {
...state,
loading: false,
success: false,
error: true,
}
case 'SUCCESS':
return {
...state,
loading: false,
success: true,
error: false,
}
default:
return state
}
}
const useHttpReducer = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return {
state,
dispatch,
}
}
export default useHttpReducer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment