A Couple React Components in Rescript
Introduction
This is part of an ongoing series about the Rescript programming language. I’ve been building a toy app and taking notes as I go along. I’m already pretty comfortable in the language, and I haven’t loved coding in anything this much since I first discovered Python.
This article is a bit of a breather in that it doesn’t have a ton of new concepts. I’m just building out the app in preparation for new content in the future. I’ll be adding components for rendering all tags and to view an individual recipe. The next article will go into detail of hooking up some forms to the actions we wrote in the previous one.
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.
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 are being 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 check out the action-reducers branch of the accompanying github repository. That is the state of the repo as of the end of the previous article. This article’s commits will go in the components branch.
As a refresher, we are building a “Recipe Book” app. So far, we have the basic Rescript boilerplate in place along with routing and navigation and some simple redux-style state. In this article, we’ll add a couple of presentational components, and in the next one, we’ll hook up some form to fire actions for those reducers.
The all tags boilerplate
You would think it would make sense to start with adding a tag so we can get
data into the reducer in order to render it. But AddTag
will be the most
complicated component in the system. In my daily work, I usually tackle the
most complicated things first because it helps make sure that my convoluted
evil plans will actually work. But when I’m learning or teaching, I tackle the
simple things. That way, I can discover something that will help me understand
the harder part later.
So we’ll start with the much simpler AllTags
route. Create a new file named
AllTags.res
. Remember the note in a previous article about starting a module
file with a capital letter. I’m putting all the components directly in the
src/
directory since it’s not a very big project, but you can create a folder
hierarchy if you prefer.
Next, whip out a quick dummy component with no props:
@react.component
let make = () => {
<div>{React.string("All tags")}</div>
}
Then update the All tags
variant arm in App.res
to load it:
| list{"tags"} => <AllTags />
This isn’t a very useful component. We need to add some properties to the
component so we can receive a list of tags. Add open Belt
at the top of
AllTags.res
so we can use the Map.String.t
type and arrays can’t throw
exceptions.
Change the component’s function definition to
let make = (~tags: Map.String.t<array<string>>) => {
This allows us to accept a tag. Then change the accompanying line in App.res
to | list{"tags"} => <AllTags tags={state.tags} />
.
At this point, I threw a Js.Console.log(tags)
call into the make
function.
I was surprised to discover that it was undefined
- Which is apparently what
Map.String.empty
compiles to! - See Store.bs.js
if you don’t believe me!
Sample data
Before we can render tags, we need some tags to actually render.
There are a couple ways we could get this:
- Easiest: Put it in
initialState
directly - Hardest: Implement the add recipe and add tag forms first
- Most educational: dispatch the necessary actions to watch the state change We
can do this with a
useEffect
hook in ourApp.res
. I like educational, so let’s choose this route!
Here’s the hook (put it just below the React.useReducer
call in App.res
):
React.useEffect1(() => {
dispatch(
Store.AddRecipe({
title: "Bread",
ingredients: "flour, salt, water, yeast",
instructions: "Mix, let rise overnight, bake at 400",
}),
)
dispatch(Store.AddTag({recipeTitle: "Bread", tag: "carbs"}))
None
}, [dispatch])
Even if you’re used to the useEffect
hook in Javascript, this may need some
explaining. (And, if you aren’t used to useEffect, read
here). Rescript-react makes a
couple subtle changes to the API that you need to know about.
First, note how the method is named useEffect1
. The 1 is because the deps
array [dispatch]
contains one argument. If you had two arguments it would be
useEffect2
and you’d use a tuple (arg1, arg2)
.
Rescript doesn’t have the concept of an array of arbitrary types and arbitrary
length So they use tuples instead, which allow arbitrary types, but not
arbitrary length. So the method name has to encode that arbitrary length. If
you aren’t confused enough yet (I am), useEffect1
is special because there is
no such thing as a tuple with a single value, so for this special case, we pass
an array instead.
This API is not ergonomic and I don’t like it. But I understand why it’s necessary to get other benefits of Rescript. Provable type safety is great, but the thing I really love is the speed. If they had to slow the compiler down to make this API more appealing, I’d prefer fast and ugly!
Another difference you should notice is that the method explicitly returns
None
. In Javascript, useEffect
can return either undefined
or a function
that can be used to clean up the effect.
In Javascript, undefined
is returned implicitly (if you don’t return anything
from a function) so you never see return undefined
in your effects. But
Rescript doesn’t have the concept of undefined
, so your rescript UseEffect
has to return either None
or Some(() => ...)
.
In our case, there’s no cleanup required, so we return None
.
Inside the effect body, we are dispatching two events:
- one to add a recipe
- one to add a tag to that recipe
The dispatch is just a function call, and contains a variant for the action that we defined in the previous article.
my Js.Console.log(tags)
statement is no longer ‘undefined’
You may want to add some more tags and recipes inside the hook during this testing phase.
(That was a legitimate reipe, btw;I make it all the time!)
Fleshing out the All Tags view
Now we can render the list of tags and recipes. This will look a little weird,
but it’s fairly idiomatic Rescript. We’ll be really exercising the ->
pipe
operator.
- Replace the
AllTags.res
open Belt
@react.component
let make = (~tags: Map.String.t<array<string>>) => {
let tagComponents =
tags
->Map.String.toArray
->Array.map(((tag, recipes)) =>
<div key={tag}> <h2> {React.string(tag)} </h2> <RecipeList recipes /> </div>
)
->React.array
<div> {tagComponents} </div>
}
(Note: I know RecipeList
isn’t defined, yet. We’ll get to it in a moment.)
This shows how the pipe operator should really be used. The output of one ->
becomes the input of the next one. I’m finding I write a lot of code that has
four or five levels of pipes. It’s sort of like the a.b.c.d
syntax in
Javascript.
One thing that might trip you up is that when piping to a function with only
one argument, you leave off the surrounding ()
.
It reads like a series of transforms:
- Take the tags map and transform it into an array
- transform that array into an array of tags
- transform that array into a
React.array
The Array.map
function is similar to a map in Javascript; it applies a
function to each of the elemnts in the array and returns an array of whatever
that function returns.
The React.array
type is kind of like the React.string
we saw in the first
article. It allows you to guarantee that the thing you are passing in is an
array of React components.
A little comment about formatting: I wrote it with the
<div key={tag}>...</div>
all on separate lines. I think it was more readable
like that, but the rescript formatter insisted on merging it.
However, one of my core tenets is “never argue with the default formatter”. Arguing about formatting (whether with other coders or with the formatting software) is a colossal waste of time: more time than is wasted by trying to read the output.
One more neat feature to note is that you can pass a recipes
prop without
specifying the value. In traditional jsx, this would be <RecipeList recipes={recipes} />
This is known as punning
I don’t know why, but it’s a funny word.
not currently compiling.
RecipeList
We’re still missing the RecipeList
component. Let’s add that so the compiler
stops complaining. You might want to try this yourself before looking at my
code.
Cretae a new RecipeList.res
file as follows:
open Belt
@react.component
let make = (~recipes: array<string>) => {
<div>
{recipes
->Array.map(recipe =>
<div key={recipe} onClick={_ => RescriptReactRouter.push(`/recipes/${recipe}`)}>
{React.string(recipe)}
</div>
)
->React.array}
</div>
}
This just takes the array of recipes and passes it into map to create an array
of components. I’ve added a RescriptReactRouter.push
call like we used in the
NavBar
so the recipes can link to the relevant view.
Now visit (http://localhost:3000/tags)[http://localhost:3000/tags] to see the
tags you defined in the useEffect
earlier. It should look like this:
And clicking the recipe titles should take you to an individual view recipe
component.
One more react component (View Recipe)
We’ll do one more top-level component. Create a new file ViewRecipe.res
with
some dummy content:
@react.component
let make = (~state: Store.state, ~title: string) => {
<div> {React.string("View Recipe " ++ title)} </div>
}
Update the route matching in App.res
:
| list{"recipes", title} => <div> {<ViewRecipe state title />} </div>
Note the elegant punning for passing state and title. This is great; I do so dislike unnecessary code repetition.
The page is loading fine in my browser, so we can quickly hop to customizing it. Have you noticed yet how lightning fast Rescript is to compile? I’m not sure if I’ve mentioned before how much I love it. ;-) It gets everything recompiled before the react hot loader notices anything has changed.
First, let’s look up the recipe in the state (this won’t compile yet):
open Belt
@react.component
let make = (~state: Store.state, ~title: string) => {
let recipe = state.recipes->Map.String.get(title)
<div> {React.string("View Recipe " ++ recipe.title)} </div>
}
“We’ve found a bug for you!” the compiler announces, and indeed it has.
If I had been writing raw JS I may have forgotten that the recipe
value might
be undefined.
Rescript won’t let me do that because the recipe
returned by get
is
actually an option.
Let’s fix it to handle the None
case correctly:
open Belt
@react.component
let make = (~state: Store.state, ~title: string) => {
switch state.recipes->Map.String.get(title) {
| None => <div> {React.string(title ++ " is not in our database")} </div>
| Some(recipe) => <div> {React.string("View Recipe " ++ recipe.title)} </div>
}
}
Rendering an individual recipe
Displaying the actual recipe involves rendering four pieces:
- title
- ingredients
- instructions
- tags
For readability, let’s put this logic into its own function, given a recipe as a prop:
let displayRecipe = (recipe: Store.recipe) => {
<div>
<h2> {React.string(recipe.title)} </h2>
<div>
<h3> {React.string("Ingredients")} </h3> <div> {React.string(recipe.ingredients)} </div>
</div>
<div>
<h3> {React.string("Instructions")} </h3> <div> {React.string(recipe.instructions)} </div>
</div>
<div>
<h3> {React.string("Tags")} </h3>
<div> {recipe.tags->Array.map(tag => <div> {React.string(tag)} </div>)->React.array} </div>
</div>
</div>
}
Note that this function is not a @react.component
; it just returns JSX.
Either would work, but for a simple component like this, it didn’t seem necessary.
I’d do the same in standard react in Javascript.
Calling this function inside the switch statement brings no surprises:
@react.component
let make = (~state: Store.state, ~title: string) => {
switch state.recipes->Map.String.get(title) {
| None => <div> {React.string(title ++ " is not in our database")} </div>
| Some(recipe) => displayRecipe(recipe)
}
}
An exercise for the reader
I’m getting bored with writing presentational components, so I’m going to pass
the torch to you so I can start writing about the Add Recipe
form. Here are
a couple things you might consider adding to the current code:
- Consider adding a new route for
/tags/<tagname>
to view a list of tags- You can reuse the
RecipeList
component - You can add links to individual tags from inside the
ViewRecipe
component
- You can reuse the
- Maybe add a random recipe selector to the home page
- I should have added a recipe
id
to the recipe state- You could try adding it
- You’d need to add a
nextId
integer type to the state - change all the urls to go by id instead of title
Conclusion
This article created a few new components. We saw how to add properties to components and some new uses of the pipe operator. The current state of the project can be found on github in the components branch.
At this point, I can definitively say that I love this language. I have to write Typescript in some other projects and it feels so annoying now. Six months ago when I started writing Typescript, I felt like I was in heaven, compared to standard Javascript! That’s how much better Rescript is.
I really hope the project takes off, and that this articles help make that happen.
If you like this content, and want to see more of it, I humbly ask you to check out my patreon. I will be posting articles two weeks early for subscribers to give me feedback.
I’m not looking for this to be a major money maker. It’s been my pleasure to write these articles lately, as I’ve been between jobs. However, once I’m back to work, it’s going to require some extra motivation to keep me at it. If you want to see the work, please do subscribe to show your appreciation.
More excitingly, if I get enough subscribers, I promise to turn this content into a book! :-)