Using ReScript in a big production project

Hey folks!

At my work we’re at the beginning of a new big react native project. Due to the fact that we have an emphasis on functional programming I would like to incorporate a language that fits our needs.

I am uncertain if ReScript is ready to use for such a big project, or that it would be wiser to just use ReasonML at the moment and convert the codebase to ReScript at a later moment.

What is everyones thought on this?
Would you choose to start right away with ReScript, or just wait and use ReasonML first? And why?

Just a word about RN support: only ReScript will receives updates, ReasonML bindings will stick to 0.64 and won’t receives any update as codebase of this project as migrated to ReScript.
I don’t see any actual reason to start a new project with ReasonML instead of ReScript.

Welcome :wave:

First of all, is your team familiar with React Native / React / ReScript? If not, I’d highly recommend to learn each technology separately first, because you won’t get anything done if you are trying to juggle too many new things at once.

Don’t choose ReScript just for the “sake of doing FP”. ReScript priorities the pragmatic approach, so you will not find any typical FP primitives you’d usually find in other functional programming languages. We also go with the t-first convention, which means that we kinda discourage currying. We are also not that religious about “pureness” and use imperative loops where it makes sense.

That said, we have an ML based type system, algebraic data types (aka variants), pattern matching, immutable records, and a pipe operator that pipes into the first argument position (just in case if this is what you meant by “FP”).

ReasonML is slowly fading out of the ReScript platform and only kept for legacy reasons. If you want to build a product, I’d recommend using the ReScript syntax to get support from the team and the community.

The only thing that makes sense for me to start a project with reason syntax is that it’s more mature IMO.
there are some minor parser bugs which might be okish for majority of users.

Just got done writing a new decently sized feature in ReScript.

  1. Blocked UI until request data was available
  2. Rendered form fields, some of which could only be populated when previous form fields are selected
  3. Calculated pricing data as a user updated the form into a sidebar
  4. Drafted a custom slider UI from scratch in ReScript powered by a state machine using variants + React effects
  5. Validation and submission also using a state machine which causes polling when the submission is accepted for status updates

I did pull in the Jotai bindings for state\reactivity, but I’m sure other viable options exist as well. The project went very well overall. I was about a week late from my two-week estimate but I did add a lot of polish (including a konami code easter-egg also powered by a state machine), a fully responsive UI, and built out enough pieces that the 3 upcoming projects derived from this UI should be able to be knocked out in a day each.

Overall I would describe the experience as pretty pleasant and had a measurable impact compared to a similar feature-set in the previous project which came out several weeks late and was only built to the barely-working state. Previously, I would be stuck in loops of “How do I get this to work?”, in ReScript I found myself thinking more often, “How should this ideally work? What can I do to make it better?” which is a much better place to be in.

I’ve some experience working with ClojureScript for comparison. Similarly to ReScript I find myself also focusing more about making things better than how to make a feature work. In contrast though I find cljs better for getting something out the door as fast as possible where as ReScript I find the experience a bit slower but with the added bonus being inherently safer. ClojureScript can be made a bit safer but it takes more discipline and additional work, that said it’s still several magnitudes safer than vanilla JS thanks to its polymorphism.

Learning ReScript

I’m a total noob at TS but was somehow able to pick up ReScript as I went along. It took about two days of drafting React components before it started to click and I could really move with it.

Pain Points

There were only a few minor pain points I encountered:

  1. Libraries like bs-jest are still written with ReasonML in mind so instead of value->expect->toBe(3) you end up with toBe(3, value->expect) unless you use the old |> last-arg pipe operator. I have forked the library and am slowly trying to upgrade it to ReScript. No estimate on that, stuck on a few things.

  2. Sometimes bindings are not worth the effort in ReScript. For example I had to run code like the following:

    let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
    nativeInputValueSetter.call(input, 'custom value');
    

    A co-worker attempted the ReScript bindings for learning purposes:

    https://rescript-lang.org/try?code=PTAEBEFMDMEsDtKgC4AskFsD2ATArgDZLICeADsVqKrDseqHgOYEmiwZlEaTzICGyWFnig6A2AQDOAKGz4ioAJLwyeZAC5QAbxmhQRZKHiDYAN0gBlSMhVrkANX4E8kLQApwWDADpUyDAI7dQBRbl5kABpQKWQAJwQmAEpQAF4APkZ4WGQZAF80nT0UciQsACMAK0gAY2QAYQJ+KVl9UgpQAHcEHCxOwq9fbvhezuL2pH9A4OQwyB4+RubWko6poNVQ8L4ABTisZAPS8dL2TeQ9rAo40igpGoSyQ7iTjoR7S+vSa2RkSDjCrEEvAmGlMnhsrligABMzOUCQAAefziJgIXR6fS0w1GhQARDi+niZDCmDZikiUWjQGTbOcli1sZj+hlqAENvY5gsGk0WviABIAFQAsgAZGZciLE0nk-SU-7Usj7Q4TLTrCXbHnLMFs6bnSW7ZVHDqpYr6PFKg7GyDS-Sw+Hy1HwirVOoMqRaF21LV81KgPEAeSq3ttoGhUl4OApyIV8NpAc68E+-1ukHuj2eHjNoC9bt5Uki2YA5PxC-p9EDEmXQClWe91MmbiQ7g9YE8sAC-Xj44nG6n023nqHobSETGneiI5ozh99l9m2nW+3O5l6xc5ymSD8Ufip8OIyNo1T4TVnAQPGu+1ubCjooM-OyNfMItFKyDa5ki53s3jTwQCKGhjGKYFgzE4LhWDe-xpNmuY+is+gALTpD2SYbk2LYZh27iEp0yG0jM7rIZaKqlNEeJwhBeJJNmyFTiS+hASYQgWD8YHOK4hTuGu0SUa4H7Zsx5iQOxEHbv8yF-gQ3HnLxHGQEkyGwEw8AdpA+QyEAA

    Instead I decided to draft it in TypeScript:

    /**
     * Useful if aiming to trigger an input event on a React.component. React
    * overwrites an input element's .value setter so that triggering an input event
    * after will not fire as intended
    *
    * This gets the native setter and applies it to the input element bypassing the
    * react overrides.
    *
    * See the stack overflow link below for a thorough explanation
    * https://stackoverflow.com/a/46012210
    *
    * @param input - <input> element
    * @param value - Value to set
    */
    export function nativeSetInputValue(input: HTMLInputElement, value: string):void {
    	const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
    	nativeInputValueSetter?.call(input, value);
    }
    

    and write a single ReScript binding:

    @module("@crunchy/shared/utils/form-utils")
    external nativeSetInputValue: (Dom.element, string) => unit = "nativeSetInputValue"
    

    So I think there will be times where a direct ReScript binding may not be the best tool for the job.

  3. The other pain point comes from when a strict typing system fights against intended flexibility. For example we have a small util for multiple optional class names:

    <div className={joinClassNames("baseClass", styles.activeOrUndefined, isActive && "active")}>
    Some content
    </div>
    

    In ReScript the behavior is doable but the interface is not as pleasant imo:

    <div 
    	className={Utils.classNames([ 
    		Js.Nullable.return("baseClass"), 
    		styles["activeOrUndefined"], // Seems to return a Js.Nullable inherently
    		isActive ? Js.Nullable.return("active") : Js.Nullable.null
    	])}>
    {"Some content"->React.string}
    </div>
    

    Which means this also might benefit from being drafted in TS and using a looser ReScript binding to get a more forgiving interface.

  4. I keep forgetting to do <span>{"Content"->React.string}</span> or <span>{["Some", "Array"]->React.array}</span> vs <span>Content</span>. Hopefully that habit will form with more time and practice?

Summary

In short, I’m happy a co-worker recommended switching our stack to ReScript and look forward to migrating more of our production app into it.

9 Likes

This was a great read! Just curious, how did you go about integrating ReScript into (what sounds like) a typescript project? Are you checking in the .bs.js files?

Thanks! We had a really tough time trying to sell ReScript to the head of the team. They have a lot of contacts in Silicon Valley and had inquired about ReScript. Seems the reception was that no one heard of it and they were not recommending it. My coworker and I felt strongly though that it will likely be a productive alternative for our team.

At some point we came to the conclusion that no matter how many written arguments we could make, they are just not going to convince anyone. Fortunately I did break through in a 1-on-1 where I mentioned it’s much different than other compile-to-JS because we can transpile to JS\TS at development time and it produces JS + TS we can consume from TS when needed. Also pointed out we could adapt it gradually instead of requiring everything to be rewritten at once. At that point the head was more willing to give it a shot, so it was up to my coworker and I to prove its value with this project.

First my coworker and I started each with a prototype in a separate repo, within a couple of days we both had a working prototype to show which was an extremely promising sign early on. Once that had some momentum I continued work on mine to bring it closer to production quality and stood up a mock API server in ReScript + Express to simulate the API responses we were expecting. After that was all working the coworker and I paired to move it into our production repo and wire it to the actual API. We got that done in a couple of hours and quickly had something to show again!

For project setup we chose to commit our .bs.js files as it meant not having to change our build system in anyway which was an easier sell to the head to try it out. That said, I think adding a build step in our github actions is a good idea down the road. Recently, some of our backend team had updated an API response in the ReScript but didn’t run the tooling to generate the .bs.js files.

2 Likes

Our main server for Day One is written in Reason, and it’s pretty big. Over 100 endpoints. We’ve upgraded much of it to Rescript already. We continue upgrading as we go. I recommend sticking with Rescript over Reason for sure.

1 Like

Note the introduction of .bs.js was to help the build system remove stale build artifacts easier. We improved the algorithm in the last few releases, so you can just use plain .js or .mjs if you decide to check in the generated code (recommended practice)