For weeks I was romanticizing ReasonML but didn't find an opportunity so far to actually try it out π
Then I started working on the contact form of a new project I'm doing in GatsbyJS, which I started using useState
hooks but then decided to use useReducer
for the first time, to get a more state machine like experience, when I started to remember Reason's beautiful Pattern Matching and couldn't resist any longer π
The Problem
I'm new to ReasonML & Bucklescript and am using Theme UI for styling, which I think is a little more complicated to use in Reason due to the custom JSX Pragma & sx
prop magic β¨
Please let me know if you know good ways to integrate/bind.
Render Props to the rescue
So I'm using Render Props to connect logic & styling.
I don't use them often, but they can be pretty useful at times. π
For example, I have a layout component which wraps most pages, takes in the original page props and passes certain helpers down/back if the child is a function. This way I can save on state-management/context. π
Before
Just for reference that's the pure JS contact form I started out with.
/** @jsx jsx */ import { jsx } from 'theme-ui' import { useReducer } from 'react' import isEmail from 'validator/es/lib/isEmail' import { InputField } from './input-field' const initialValue = { status: 'idle', errors: {}, values: { email: '', message: '', consent: false }, } function reducer(state, action) { switch (action.type) { case 'touched': return { ...state, status: 'touched', values: { ...state.values, ...action.values }, } case 'submitting': return { ...state, status: 'submitting', errors: {} } case 'error': return { ...state, status: 'error', errors: { ...state.errors, ...action.errors }, } case 'success': return { ...initialValue, status: 'success' } default: throw new Error() } } export const ContactForm = () => { const [{ status, values, errors }, dispatch] = useReducer( reducer, initialValue ) const collectErrors = {} const handleSubmit = event => { event.preventDefault() dispatch({ type: 'submitting' }) const cleaned = { email: values.email.trim(), message: values.message.trim(), } if (!isEmail(cleaned.email)) { collectErrors.email = 'Please provide your best e-mail address' } if (!cleaned.message) { collectErrors.message = 'Please provide a message' } else if (cleaned.message.length < 20) { collectErrors.message = 'Please be more specific' } if (!values.consent) { collectErrors.consent = 'You have to agree to submit' } if (Object.keys(collectErrors).length > 0) { dispatch({ type: 'error', errors: collectErrors }) return } setTimeout(() => { dispatch({ type: 'success' }) }, 2000) } const setEmail = (_, value) => { dispatch({ type: 'touched', values: { email: value } }) } const setMessage = (_, value) => { dispatch({ type: 'touched', values: { message: value } }) } const setConsent = (_, value) => { dispatch({ type: 'touched', values: { consent: value } }) } const handleKeyDown = event => { if (event.metaKey && (event.key === 'Enter' || event.keyCode === 13)) { handleSubmit(event) } } return ( <form action="" method="post" key="ContactForm" onSubmit={handleSubmit} onKeyDown={handleKeyDown} > <fieldset disabled={status === 'submitting'} sx={{ border: 0 }}> <InputField type="email" label="E-Mail-Address" value={values.email} placeholder="mail@example.com" onChange={setEmail} errorMessage={errors.email} required /> <InputField type="textarea" label="Message" value={values.message} placeholder="Say hi π" onChange={setMessage} errorMessage={errors.message} sx={{ marginTop: '1rem' }} required /> <InputField type="checkbox" label="I agree to my e-mail address and message being stored and used to review the request Privacy policy" value={values.consent} onChange={setConsent} errorMessage={errors.consent} disabled={status === 'submitting'} sx={{ marginTop: '1rem' }} required /> <button type="submit" disabled={status === 'submitting'} sx={{ variant: 'buttons.primary', marginTop: '1rem' }} > Submit </button> </fieldset> </form> ) }
Initial ContactForm.re
I thought I'd "just" write the following in ReasonML and keep the rest in JS. This way I could progress my learning slowly and mostly leverage the cool pattern matching in my reducer. π
type status = | Idle | Touched | Submitting | Success | Error; type record = { email: string, message: string, consent: bool, }; module Errors = { type error = { mutable email: string, mutable message: string, mutable consent: string, }; }; type state = { status, errors: Errors.error, values: record, }; let initialValue = { status: Idle, errors: { email: "", message: "", consent: "", }, values: { email: "", message: "", consent: false, }, }; type action = | Touched(record) | Submitting | Success | Error(Errors.error); let reducer = (state, action) => { switch (action) { | Touched(values) => {...state, status: Touched, values} | Submitting => {...state, status: Submitting, errors: initialValue.errors} | Error(errors) => {...state, status: Error, errors} | Success => {...initialValue, status: Success} }; }; [@react.component] let make = (~children) => { let (state, dispatch) = React.useReducer(reducer, initialValue); children({ "status": state.status, "values": state.values, "errors": state.errors, "setTouched": x => dispatch(Touched(x)), "setSubmitting": () => dispatch(Submitting), "setSuccess": () => dispatch(Success), "setError": x => dispatch(Error(x)), }); }
After getting this to work and feeling comfortable enough I decided to handle all the logic in ReasonML π
open ReactEvent.Keyboard; [@bs.module "validator/es/lib/isEmail"] external isEmail: string => bool = "default"; [@bs.val] external setTimeout: (unit => unit, int) => unit = "setTimeout"; /* I modified it to return unit instead of float because of some error I got but don't remember right now and is only used to fake an async submit until I implement the actual logic */ type status = | Idle | Touched | Submitting | Success | Error; type record = { email: string, message: string, consent: bool, }; module Errors = { type error = { mutable email: string, mutable message: string, mutable consent: string, }; }; type state = { status, errors: Errors.error, values: record, }; let initialValue = { status: Idle, errors: { email: "", message: "", consent: "", }, values: { email: "", message: "", consent: false, }, }; type action = | Touched(record) | Submitting | Success | Error(Errors.error); let reducer = (state, action) => { switch (action) { | Touched(values) => {...state, status: Touched, values} | Submitting => {...state, status: Submitting, errors: initialValue.errors} | Error(errors) => {...state, status: Error, errors} | Success => {...initialValue, status: Success} }; }; [@react.component] let make = (~children) => { let (state, dispatch) = React.useReducer(reducer, initialValue); let handleSubmit = event => { ReactEvent.Synthetic.preventDefault(event); let collectErrors: Errors.error = {email: "", message: "", consent: ""}; dispatch(Submitting); let email = Js.String.trim(state.values.email); let message = Js.String.trim(state.values.message); if (!isEmail(email)) { collectErrors.email = "Please provide your best e-mail address"; }; /* let msgLength = String.length(message); if (msgLength === 0) { collectErrors.message = "Please provide a message"; } else if (msgLength < 20) { collectErrors.message = "Please be more specific"; }; */ switch (String.length(message)) { | 0 => collectErrors.message = "Please provide a message" | (x) when x < 20 => collectErrors.message = "Please be more specific" | x => ignore(x) }; if (!state.values.consent) { collectErrors.consent = "You have to agree to submit"; }; /* Not my best work π showing alternative syntax |> & -> I'm using the latter in my "real" code it's in this case a little more concise as it formats nicer a little bit confusing maybe π€, also I don't like this formatting actually π€·ββοΈ */ if (String.length(collectErrors.email) > 0 || collectErrors.message |> String.length > 0 || collectErrors.consent->String.length > 0) { dispatch(Error(collectErrors)); } else { /* Submit logic has yet to come as I'm focusing on UI first */ setTimeout(() => dispatch(Success), 2000); }; }; let handleKeyDown = event => if (event->metaKey && (event->key === "Enter" || event->keyCode === 13)) { handleSubmit(event); }; let status = switch (state.status) { | Idle => "idle" | Touched => "touched" | Submitting => "submitting" | Success => "success" | Error => "error" }; let props = { "status": status, "values": state.values, "errors": state.errors, "setTouched": x => dispatch(Touched(x)), }; <form action="" method="post" key="ContactForm" onSubmit=handleSubmit onKeyDown=handleKeyDown> {children(props)} </form>; }; let default = make;
Most stuff looks more or less okay I guess. Only thing I'm reeaally not sure but didn't manage to find another solution right away is all the collectErrors
stuff.
There's maybe, better ways I just don't know yet ππ€·ββοΈ Once I do, maybe because of nice feedback (via Twitter) I'll come back to improve it.
Uh and I tried to pass more specific helper functions like setMail
down to children
but couldn't get the reducer for them working so far.
JS file just for styling purpose
/** @jsx jsx */ import { jsx } from "theme-ui"; import { InputField } from "components/input-field.js"; import { make as ContactFormLogic } from "components/ContactForm.bs.js"; export const ContactForm = () => ( <ContactFormLogic> {({ status, values, errors, setTouched }) => ( <fieldset disabled={status === "submitting"} sx={{ border: 0 }}> <InputField type="email" label="E-Mail-Address" value={values.email} placeholder="mail@example.com" onChange={(_, value) => setTouched({ ...values, email: value })} errorMessage={errors.email} required /> <InputField type="textarea" label="Message" value={values.message} placeholder="Say hi π" onChange={(_, value) => setTouched({ ...values, message: value })} errorMessage={errors.message} sx={{ marginTop: "1rem" }} required /> <InputField type="checkbox" label="I agree to my e-mail address and message being stored and used to review the request Privacy policy" value={values.consent} onChange={(_, value) => setTouched({ ...values, consent: value })} errorMessage={errors.consent} disabled={status === "submitting"} sx={{ marginTop: "1rem" }} required /> <button type="submit" disabled={status === "submitting"} sx={{ variant: "buttons.primary", marginTop: "1rem" }} > Submit </button> </fieldset> )} </ContactFormLogic> );
Thoughts on ReasonML
I really enjoy using it, not being able to spread props or multiple times into objects/records is still a little confusing. But that's a trade-off I'm willing to accept.
Actually I'm looking forward to a stable release of elodin by @robinweser probably in conjunction with fela to replace ThemeUI and drop the additional JS file. We'll see..
How I do ReasonML in GatsbyJS
I started with the help of gatsby-plugin-reason only to discover it's pretty outdated and bs-loader
is not even recommended anymore.
Took me a while to figure this out though while trying to understand why nothing was working^^
Installing ReasonML & ReasonReact in an existing GatsbyJS project
yarn install reason-react && yarn install -D bs-plattform
By the way I'm on reason-react@0.7.0 & bs-platform@7.2.2
bsconfig.json
{ "name": "PROJECT_NAME", "reason": { "react-jsx": 3 }, "bsc-flags": ["-bs-super-errors"], "sources": [ { "dir": "src", "subdirs": true } ], "package-specs": [ { "module": "es6", "in-source": true } ], "suffix": ".bs.js", "namespace": true, "bs-dependencies": ["reason-react"], "ppx-flags": [], "refmt": 3 }
package.json
{ "scripts": { "re:build": "bsb -make-world -clean-world", "re:start": "bsb -make-world -clean-world -w", "re:clean": "bsb -clean-world" } }
That's actually it.
Pretty useful links
Official
By Dr. Axel Rauschmayer (@rauschma)
- Pattern matching in ReasonML: destructuring, switch, if expressions
- ReasonML: records
- Archive of all #ReasonML articles
By others
- Reason Testing Library
- The BuckleScript Cookbook β by glennsl
- Gist explaingin pipe-first (->) vs pipe-last (|>) β by Heechul Ryu (@ryuheechul) found in Reason Discord
- Data-first and data-last: a comparison (mentioned in the former Gist) β by Javier ChΓ‘varri (@javierwchavarri)
- Best convention/style for pattern matching topic in Reason's Discord
- ReasonML for production React Apps? π€ (Part 1) β by Seif Ghezala (@seif_ghezala)
- Higher Order Function signature in Reason Discord
- Exploring Bucklescriptβs Interop with JavaScript (in Reason) Somewhat old but still valuable β by David Gomes (@Munchor)
- Learning ReasonReact Step by Step Part: 4 β by rockyourcode.com
- ReasonReact + useReducer hooks β Gist by Patrick Stapfer (@ryyppy)
- reason-hooks-lib as inspiration β by Victor Wang (@HelloVictorWang)
- Use ReasonMLs Variant & Record types with ReasonReact's useReducer hook to Manage State β by Thomas Greco (@tgrecojs) on egghead.io
- ReasonML with React Hooks Tutorial β Building a Pomodoro Timer β by Ian Wilson (@iwilsonq)
- ReasonML PPX β by GrΓ©goire Vda (@gregoirevda)
- Vanilla JSX in Reason in Reason Discord
- TDD A REASONML FUNCTION β by Jake Trent (@jaketrent)
- Next.js, but in ReasonML and Bucklescript! if you're using NextJS β by Murphy Randle
- re-tailwind β by Hieu Pham (@phthhieu)
πΈ Cover image by Victor Garcia on Unsplash
Top comments (0)