Firing Actions From Rescript React Components
Introduction
This is part of an ongoing series about the Rescript programming language. I’m taking notes as I build a toy “Recipe Book” app to explore the Rescript ecosystem.
This article adds a couple new React components. They are more complicated than the ones in the previous article because they are managing state and fire actions on form submit. But we’ll find there is still a minimal amount of boilerplate, and Rescript is continuing to bring me scads of delight.
Patreon
This series takes a lot of time to write and maintain. I started it while I have some free time between jobs, but I’ll probably need some extra motivation to keep working on it once I’m busy with work again.
It’s become popular enough that I’ve already fielded a few change requests that have taken additional time. This has slowed me down on new content, but it makes the existing content a lot better.
I’m not one for begging for money on the Internet, but if you’re interested in providing some of that motivation, I’ve set up a Patreon account. I thank you for your support.
As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.
Other articles in series
With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.
Let’s begin
If you haven’t been following along with these articles, you can git checkout
the
components
branch of the accompanying github
repository.
If you want to see the code for this article, I make commits to the stateful-components branch that roughly map to the sections in this article. You may find it instructive to see the diff of the JS that the rescript compiler outputs with each change in your rescript code.
The Add Recipe form
Adding a recipe requires managing state around the keypresses. This can be done
easily with the UseState
hook, as is standard with React functional component
forms. But first, let’s rough out the visual parts of the AddRecipe
form.
Create a new AddRecipeForm.res
file:
@react.component
let make = (~dispatch: Store.action => unit) => {
<div>
<div> <input placeholder="Title" /> </div>
<div> <label> <h3> {React.string("Ingredients")} </h3> <textarea /> </label> </div>
<div> <label> <h3> {React.string("Instructions")} </h3> <textarea /> </label> </div>
<button> {React.string("Add!")} </button>
</div>
}
None of this was unexpected for me, and I hope it doesn’t surprise you, either.
Note that we are passing in the dispatch
function as a property. It gets
passed in from the App
s useReducer
through the router switch.
Update the App.res
router’s "recipes", "add"
route to
| list{"recipes", "add"} => <AddRecipeForm dispatch />
There are three text input fields which are currently unmanaged by React. We’ll need to access the state of those fields to submit our action, so let’s turn them into stateful components.
Start with three separate useState
hooks at the top of the function:
let (title, setTitle) = React.useState(() => "")
let (ingredients, setIngredients) = React.useState(() => "")
let (instructions, setInstructions) = React.useState(() => "")
The useState
hook in normal React can accept either an initial value or a
callback that produces an initial value. However, the Rescript binding only
permits the callback version. This is why the three useState
calls have () => ""
passed into them. useState("")
would be a compiler error.
The return value of useState
is a tuple of the managed state and a function
to set new state. We destructure these into two separate variables for each
state.
The next step is to arrange for the set*
functions to be called in the
onchange handlers for the inputs. This took me a bit of ferreting about to
figure out. I’ll show the code first and then explain it:
<input
placeholder="Title"
value={title}
onChange={event => {
let title = ReactEvent.Form.target(event)["value"]
setTitle(_ => title)
}}
/>
I’ve expanded the original title
input with some new logic. First, the
current title
state is passed in as the input’s value. Any time title
changes, the component will automatically be updated.
Second, the onChange
handler is called. It accepts the form’s event
and
wraps it in ReactEvent.Form.target
whose purpose is primarily to tell
rescript what type the event has.
The return value of that function is a Rescript object. Looking up an attribute
on a Javascript object is done with index notation, hence the ["value"]
.
We bind to the extracted title using let title =
. This step may not seem
necessary, but it is; it fails if you do
setTitle(ReactEvent.Form.target(event)["value"])
.
Finally, we call the setTitle
function. Even this looks different from
standard react. The parameter must be supplied as a callback instead of
passing it in directly. Don’t worry about forgetting that, though. The rescript
compiler will remind you. This callback is the reason you had to bind to
let title =
in the previous line. The variable needs to live long enough for the
callback to execute, so it needs to be captured in the closure.
With one component completed, it’s quite straightforward to add state
management to the two textarea
s. The code is a little long (Rescript’s
formatter finally started wrapping my components once they got big enough), but
has no new concepts:
@react.component
let make = (~dispatch: Store.action => unit) => {
let (title, setTitle) = React.useState(() => "")
let (ingredients, setIngredients) = React.useState(() => "")
let (instructions, setInstructions) = React.useState(() => "")
<div>
<div>
<input
placeholder="Title"
value={title}
onChange={event => {
let title = ReactEvent.Form.target(event)["value"]
setTitle(_ => title)
}}
/>
</div>
<div>
<label>
<h3> {React.string("Ingredients")} </h3>
<textarea
onChange={event => {
let ingredients = ReactEvent.Form.target(event)["value"]
setIngredients(_ => ingredients)
}}
value={ingredients}
/>
</label>
</div>
<div>
<label>
<h3> {React.string("Instructions")} </h3>
<textarea
onChange={event => {
let instructions = ReactEvent.Form.target(event)["value"]
setInstructions(_ => instructions)
}}
value={instructions}
/>
</label>
</div>
<button> {React.string("Add!")} </button>
</div>
}
Load that in the browser to double check that it is updating fields correctly.
Now we can actually update the state when the user clicks the button. All we need to do is call dispatch. Oh, and add a redirect to the newly added recipe’s page:
<button
onClick={_ => {
dispatch(
Store.AddRecipe({title: title, ingredients: ingredients, instructions: instructions}),
)
RescriptReactRouter.push(`/recipes/${title}`)
}}>
{React.string("Add!")}
</button>
The dispatch is pretty straightforward. We construct a Store.AddRecipe
variant with the stateful values and pass it in. The call to
RescriptReactRouter.push
is the same RescriptReactRouter.push
function we
used in the navbars to handle the redirect.
Another form: AddTag
This section will be quick and straightforward. It’s using the exact same logic
as AddRecipeForm
: manage some state, dispatch an event.
Interesting note: I wrote this entire file without testing anything in the browser. Once Rescript had told me it had compiled successfully, I had enough confidence that it would just work. Considering that rescript compiles faster than React’s hot module reloader can reload, this is an amazingly tight feedback loop.
Here’s the entire AddTag.res
file:
@react.component
let make = (~recipeTitle: string, ~dispatch: Store.action => unit) => {
let (tag, setTag) = React.useState(() => "")
<div>
<input
placeholder="Add tag..."
value={tag}
onChange={event => {
let tag = ReactEvent.Form.target(event)["value"]
setTag(_ => tag)
}}
/>
<button
onClick={_ => {
dispatch(Store.AddTag({recipeTitle: recipeTitle, tag: tag}))
}}>
{React.string("Add Tag!")}
</button>
</div>
}
Unlike AddRecipe
, this component isn’t a standalone page. It can be added to
the ViewRecipe
form under the Tags div
:
<div>
<h3> {React.string("Tags")} </h3>
<div> {recipe.tags->Array.map(tag => <div> {React.string(tag)} </div>)->React.array} </div>
<AddTag dispatch recipeTitle={recipe.title} />
</div>
There also needs to be a bit of housekeeping to make dispatch
accessible to
the component. First, add dispatch: Store.action => unit
to the
displayRecipe
function signature.
Then you’ll need to update the ViewRecipe
component constructor’s signature:
let make = (~state: Store.state, ~title: string, ~dispatch: Store.action => unit) => {
Note that this change uses a named property ~dispatch
, whereas the previous
one was a positional parameter, dispatch
. (The difference is in the ~
.)
Properties in react components must always be named parameters.
displayRecipe
, though it returns JSX, is not a component proper.
Pass dispatch
into the displayRecipe
function call in ViewRecipe
, and add
dispatch
to the list of properties ViewRecipe
accepts in App.res
.
I won’t show the code for this, but you’ll be impressed at how well the linter guides you to find all these changes once you make the first one. If you get lost, check the relevant commit on the github branch. (I learned the hard way not to link directly to github commits. It makes rebasing impossible!)
Conclusion
This was a pretty short article (for me), but it covers a lot of new content. We learned how to dispatch actions in components and to manage state. The trickiest bit (for me) was figuring out how to hook up the form events.
In the next part, we’ll be covering styling.
As a reminder, if you would like to show some support for my work, you can find me on patreon. I publish all my articles there at least two weeks before posting them to my public blog. I’m not looking to make a living from this, but the amount of value my readers place on my free time directly corrolates to how much of my free time I spend on it. At the moment, I’m unemployed and I’m enjoying this project. Once I start work, it’s going to take more incentive to keep me working on this project.
On the other hand, if I get enough support on Patreon, I promise to turn this collection into a well-edited and properly designed book!