├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── images │ ├── formik.png │ ├── netlify.png │ ├── octocat.jpg │ └── netlify-logo-alt.png ├── App.test.js ├── index.css ├── index.js ├── App.js ├── serviceWorker.js └── FormikForm.js ├── .gitignore ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimfucious/netlify-forms-formik/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/formik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimfucious/netlify-forms-formik/HEAD/src/images/formik.png -------------------------------------------------------------------------------- /src/images/netlify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimfucious/netlify-forms-formik/HEAD/src/images/netlify.png -------------------------------------------------------------------------------- /src/images/octocat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimfucious/netlify-forms-formik/HEAD/src/images/octocat.jpg -------------------------------------------------------------------------------- /src/images/netlify-logo-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kimfucious/netlify-forms-formik/HEAD/src/images/netlify-logo-alt.png -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "NetliFormikify", 3 | "name": "Netlify Forms with Formik", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#36B1BB", 14 | "background_color": "#fefefe" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.development.local 17 | .env.local 18 | .env.production.local 19 | .env.test.local 20 | .vscode 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | touch-action: manipulation; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-formik-test", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "formik": "^1.5.4", 8 | "qs": "^6.7.0", 9 | "react": "^16.8.6", 10 | "react-dom": "^16.8.6", 11 | "react-scripts": "3.0.0", 12 | "reaptcha": "^1.4.2" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import netlifyLogo from "./images/netlify-logo-alt.png"; 4 | import formikLogo from "./images/formik.png"; 5 | import octocat from "./images/octocat.jpg"; 6 | import FormikForm from "./FormikForm"; 7 | 8 | const App = () => { 9 | return ( 10 |
11 |
12 | netlify logo 13 | 14 | + 15 | 16 | formik logo 17 |
18 |
19 | netlify logo 20 | + 21 | formik logo 22 |
23 |
27 | Netlify Forms with Formik 28 |
29 |
30 | 31 |
32 | 33 | Octocat 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | NetliFormikify 26 | 27 | 28 | 39 | 40 |
41 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/FormikForm.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import { ErrorMessage, Field, Form, Formik } from "formik"; 3 | import Reaptcha from "reaptcha"; 4 | import axios from "axios"; 5 | import qs from "qs"; 6 | 7 | export default () => { 8 | const [errMsg, setErrMsg] = useState(""); 9 | const [executing, setExecuting] = useState(false); 10 | const [formValues, setFormValues] = useState({}); 11 | const [formReset, setFormReset] = useState({}); 12 | const [loaded, setLoaded] = useState(false); 13 | const [msgSent, setMsgSent] = useState(false); 14 | const [rcError, setRcError] = useState(""); 15 | const [isSubmitting, setIsSubmitting] = useState(false); 16 | const [token, setToken] = useState(""); 17 | const [verified, setVerified] = useState(false); 18 | 19 | const rcRef = useRef(null); 20 | 21 | useEffect(() => { 22 | const handleSubmit = async (formValues, token) => { 23 | const data = { 24 | ...formValues, 25 | "g-recaptcha-response": token 26 | }; 27 | const options = { 28 | method: "POST", 29 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 30 | data: qs.stringify(data), 31 | url: "/" 32 | }; 33 | try { 34 | await axios(options); 35 | setMsgSent(true); 36 | formReset(); 37 | } catch (e) { 38 | setErrMsg(e.message); 39 | } 40 | }; 41 | if (token) { 42 | handleSubmit(formValues, token); 43 | } 44 | }, [formReset, formValues, token]); 45 | 46 | const onError = () => { 47 | console.log("error..."); 48 | setRcError(true); 49 | }; 50 | 51 | const onExpire = () => { 52 | console.log("expired..."); 53 | console.log("resetting..."); 54 | resetReCaptcha(); 55 | }; 56 | 57 | const onLoad = resetForm => { 58 | console.log("loaded..."); 59 | setLoaded(true); 60 | setFormReset(resetForm); 61 | }; 62 | 63 | const onVerify = token => { 64 | console.log("verified..."); 65 | setToken(token); 66 | setVerified(true); 67 | setExecuting(false); 68 | }; 69 | 70 | const renderButton = (executing, isSubmitting) => { 71 | if (errMsg) { 72 | return ( 73 | 80 | ); 81 | } else if (msgSent) { 82 | return ( 83 | 90 | ); 91 | } else { 92 | return ( 93 | 100 | ); 101 | } 102 | }; 103 | 104 | const resetEverything = resetForm => { 105 | if (rcError) { 106 | setRcError(false); 107 | } 108 | if (resetForm) { 109 | setMsgSent(false); 110 | setErrMsg(false); 111 | resetForm(); 112 | } 113 | resetReCaptcha(); 114 | }; 115 | 116 | const resetReCaptcha = async () => { 117 | console.log("resetting..."); 118 | await rcRef.current.reset(); 119 | setVerified(false); 120 | }; 121 | 122 | return ( 123 |
124 | { 132 | let errors = {}; 133 | if (!values.email) { 134 | errors.email = "Required"; 135 | } else if ( 136 | !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email) 137 | ) { 138 | errors.email = "Invalid email address"; 139 | } 140 | if (!values.username) { 141 | errors.username = "Required"; 142 | } 143 | return errors; 144 | }} 145 | onSubmit={values => { 146 | setIsSubmitting(true); 147 | setFormValues({ ...values }); 148 | setExecuting(true); 149 | rcRef.current.execute(); 150 | }} 151 | > 152 | {({ resetForm }) => ( 153 |
161 | 162 | 163 |
164 | 170 | 175 | 180 |
181 |
182 | 188 | 193 | 198 |
199 | onLoad(() => resetForm)} 207 | size="invisible" 208 | /> 209 |
210 | ReCaptcha status: 211 |
212 | 217 | loaded 218 | 219 | 224 | verified 225 | 226 | {executing && ( 227 | 228 | executing 229 | 230 | )} 231 | {rcError && ( 232 | error 233 | )} 234 | {rcError && ( 235 | 241 | )} 242 |
243 | {renderButton(isSubmitting, executing, verified)} 244 | {errMsg ?
{errMsg}
: null} 245 | {(msgSent || errMsg) && ( 246 | 252 | )} 253 | 254 | )} 255 |
256 |
257 | ); 258 | }; 259 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netlify Forms with Formik 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/46e39f1e-ce26-4619-89d2-6f2969856581/deploy-status)](https://app.netlify.com/sites/admiring-visvesvaraya-e4f018/deploys) 4 | 5 | This repo was created to test and document how to get Netlify Forms working with Formik in a Create React App build and add ReCaptcha if desired. 6 | 7 | Netlify Forms is a super cool (and free) feature for sites hosted on the [Netlify](https://netlify.com) platform. 8 | 9 | [Formik](https://www.npmjs.com/package/formik) is a great library that "removes the tears" :sob: from form creation in React. 10 | 11 | The problem is that forms rendered via React don't work out of the box with Netlify forms. 12 | 13 | ## tl;dr 14 | 15 | You need to add a hidden HTML form that mimics the fields in your Formik form in order to get Netlify forms to work. 16 | 17 | > :point_up: Note that static site generators like Gatsby and Hugo are different creatures and require a different solution (they pretty much just work) than would a Create React App build. Documentation here is solely pertinent to CRA. 18 | 19 | ## Reading material 20 | 21 | [This](https://www.netlify.com/blog/2017/07/20/how-to-integrate-netlifys-form-handling-in-a-react-app/) is a very informative article; however--for me, at least--it took a while to realize that there needs to be a (hidden) mirror HTML form of the Formik form being rendered by React. And the info surrounding reCaptcha is a bit lean. 22 | 23 | Reading [this](https://community.netlify.com/t/common-issue-how-to-debug-your-form/92) may also be helpful. 24 | 25 | ## Initial Setup (doesn't work out of the box) 26 | 27 | - [Create React App](https://github.com/facebook/create-react-app) 28 | - [Axios](https://www.npmjs.com/package/axios) with [qs](https://www.npmjs.com/package/qs) for the post 29 | - [Formik](https://www.npmjs.com/package/formik) 30 | 31 | Without extra setup, submitting a React form when hosted on Netlify will return a 404 error. 32 | 33 | This error can be a bit misleading because, when you look at the code, the form is there. It should be found. So what's happening?! 34 | 35 | The reason is that Netlify's form-bots can't see JavaScript rendered form code. It's not that they're doing anything wrong, it's just kinda how things are. Thankfully, they're nice enough to have given us a way to work around this issue. 36 | 37 | So in order to get this working, there's a few more steps to be done. 38 | 39 | ## Steps to Get Things Working 40 | 41 | ### 1) Add a static HTML version of the form 42 | 43 | Add the following form block just below the initial `` element tag in `/public/index.html`: 44 | 45 | ```html 46 | 51 | ``` 52 | 53 | According to the maesters, you can--rather than add this code block to `/public/index.html`--create a separate HTML file that includes your form code block somewhere in the build, and it will get picked up. I, haven't tried this, personally, but the maesters said it is so. 54 | 55 | > :point_up: This is obviously just an example. Your form will most likely have differing fields. So make sure that there is one-to-one match here, whereby each input corresponds with a respective input element/Field component in your Formik form. 56 | 57 | ### 2) Add additional `initial values` to the Formik form 58 | 59 | a) Add a `bot-field` and `form-name` field to `initialValues` of the Formik form: 60 | 61 | ```jsx 62 | :newspaper: In February 2019, Netlify [announced](https://www.netlify.com/blog/2019/02/12/improved-netlify-forms-spam-filtering-using-akismet/) that all form submissions will be filtered for spam, using Akismet. Huzzah huzzah! :tada: 74 | 75 | b) Add those (hidden) fields to the Formik form itself: 76 | 77 | ```html 78 | 79 | 80 | ``` 81 | 82 | > :floppy_disk: The relevant code to see how this works can be found in `/public/index.html` and `FormikForm.js` within this repo. 83 | 84 | ## Adding ReCaptcha 85 | 86 | ### tl;dr 87 | 88 | Use a library to add reCaptcha (e.g. [reaptcha](https://www.npmjs.com/package/reaptcha)) and be sure to send the reCaptcha response along with your form submission. 89 | 90 | > :scream: reCaptcha is notoriously easy to mistype, and `reaptcha` adds another nuance to the pot. I've used abbreviations in variable declarations to help avoid issues around that. 91 | 92 | ### Setup 93 | 94 | There are a lot of libraries out there for adding reCaptcha to a React site. And most reCaptcha libraries are not especially clear in their documentation, IMHO, with to how to get the end-to-end solution working, esp. getting at the reCaptcha response token. 95 | 96 | After a bit of trial and error, I settled on [reaptcha](https://www.npmjs.com/package/reaptcha). It's clean and documented well. 97 | 98 | There's actually only one key step to get reCaptcha working with Netlify Forms, which is to send the reCaptcha response token with your form data. The main work here is to get a hold of the reCaptcha response, so we can do just that. 99 | 100 | The steps are: 101 | 102 | 1. Load reCaptcha 103 | 2. Execute reCaptcha onSubmit (for reCaptcha v2 invisible) 104 | 3. Retrieve the reCaptcha response 105 | 4. Submit the reCaptcha response along with the form data. 106 | 107 | To accomplish this, this example leverages the built-in callback functions of `reaptcha` along with some React Hooks. 108 | 109 | The `Reaptcha` block looks like this: 110 | 111 | ```jsx 112 | onLoad(() => resetForm)} 120 | size="invisible" 121 | /> 122 | ``` 123 | 124 | #### onLoad 125 | 126 | `onLoad` is set as an attribute on the `Reaptcha` element in the `FormikForm.js` file. 127 | 128 | In this example, the `onLoad` callback function is used to load the `clearForm` action into a [React State Hook](https://reactjs.org/docs/hooks-reference.html#usestate) value, `resetForm`: 129 | 130 | ```jsx 131 | const onLoad = resetForm => { 132 | console.log("loaded..."); 133 | setLoaded(true); 134 | setFormReset(resetForm); 135 | }; 136 | ``` 137 | 138 | This is kinda hacky, but in order to clear the form after a successful form submission (or for some other reason), I wanted access to that action, which is passed down as a prop in the Formik block scope but is not accessible outside it. 139 | 140 | There may be (probably is) a better way to do this, but I got bored thinking about it and moved on. 141 | 142 | At this point, nothing happens until the user fills out and submits the form. 143 | 144 | #### useRef 145 | 146 | Once reCaptcha is loaded, reCaptcha needs to get executed "manually". Manually calling `execute()` is necessary for the support of reCaptcha v2 invisible, which is set via the `size` attribute on the `Reaptcha` element, because clicking on the reCaptcha widget is not possible when it's not visible. 147 | 148 | Though that may seem plainly obvious to some, I'll re-emphasize it anyhow: It's nature of the invisible reCaptcha beast to be invisible, and thus we must wire the user action of submitting the form to reCaptcha execution. 149 | 150 | > :robot: Only reCaptcha v2 invisible is documented here, but with a few tweaks you should be able to get other reCaptcha types (e.g. "I am not a robot") working. 151 | 152 | In order to execute reCaptcha, a [React Ref Hook](https://reactjs.org/docs/hooks-reference.html#useref) has been setup for the `Reaptcha` element. 153 | 154 | ```jsx 155 | const rcRef = useRef(null); 156 | ``` 157 | 158 | The above assignment, plus the associated ref attribute on the `Reaptcha` element, allows execute() to be called on `rcRef.current`, which is done when the form is submitted in the Formik `onSubmit` function. 159 | 160 | ```jsx 161 | onSubmit={values => { 162 | setIsSubmitting(true); 163 | setFormValues({ ...values }); 164 | setExecuting(true); 165 | rcRef.current.execute(); 166 | }} 167 | ``` 168 | 169 | If you're familiar with Formik, this is where all the action usually happens; however, I've moved some of the action out of this function and into a separate function, `handleSubmit`, which gets triggered by a [React Effect Hook](https://reactjs.org/docs/hooks-reference.html#useeffect). 170 | 171 | The reason for separating this out is that there is a delay between executing reCaptcha, receiving the reCaptcha response, putting it somewhere (e.g. state) where it can be accessed, and having that value ready to inject into the form data on submission. And I got tired of shooting blanks when submitting my form. There's probably a better way to do this as well. 172 | 173 | #### onVerify 174 | 175 | `onVerify` is set as an attribute on the `Reaptcha` element in the `FormikForm.js` file. 176 | 177 | The onVerify callback runs after `rcRef.current.execute()` and returns the reCaptcha response (in the form of a token). 178 | 179 | This token is stored in the `React Hook State` value `token`. 180 | 181 | #### handleSubmit 182 | 183 | In this example, a React Effect Hook, has been setup to look for `token` changes in state. 184 | 185 | ```jsx 186 | useEffect(() => { 187 | const handleSubmit = async (formValues, token) => { 188 | const data = { 189 | ...formValues, 190 | "g-recaptcha-response": token 191 | }; 192 | const options = { 193 | method: "POST", 194 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 195 | data: qs.stringify(data), 196 | url: "/" 197 | }; 198 | try { 199 | await axios(options); 200 | setMsgSent(true); 201 | formReset(); 202 | } catch (e) { 203 | setErrMsg(e.message); 204 | } 205 | }; 206 | if (token) { 207 | handleSubmit(formValues, token); 208 | } 209 | }, [formReset, formValues, token]); 210 | ``` 211 | 212 | > 🧙 If there were magic, this is where it might happen. 213 | 214 | After the onVerify callback returns the token and places it in state, the effect hook will trigger the `handleSubmit` function. 215 | 216 | `handleSubmit` builds the `axios` configuration (note the content type!) and submits the form. 217 | 218 | The reCaptcha response, `token`, gets injected into `data` just prior to form submission, and its value is assigned to the key, `g-recaptcha-response`. 219 | 220 | If axios is successful, the form gets reset with `formReset()`, which is actually Formik's `resetForm` that was populated into state at onLoad. 221 | 222 | :rainbow: And Bob's your uncle! :unicorn: 223 | --------------------------------------------------------------------------------