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)