[Updated]: How to replace React components using PureScript's React libraries

I have seen (and asked!) many questions about interop when introducing PureScript to an existing JavaScript application. I’ve now worked on rewriting an Angular app into Halogen and a React app into PureScript React and, with plenty of guidance from @natefaubion, learned a few best practices.

This post demonstrates how to take over a React component using PureScript. It’s meant for beginner-to-intermediate PureScript devs and omits no code along the way. It’s the first of three articles:

  1. Taking over individual components with PureScript React & React Basic
  2. Doing the same with Halogen
  3. Sharing more complex information like Redux stores

https://thomashoneyman.com/articles/replace-react-components-with-purescript/

If enjoyed the article & you know folks with a React codebase who are also interested in functional programming, consider sharing it with them! It’s my hope that these sorts of pieces can help teams feel more confident transitioning to PureScript with their existing applications.


2020 Update: Earlier in the year this article hit the front page of HN for a day. Plenty of people there viewed the article through the lens of Hooks, which at the time the article didn’t talk about at all. I decided to update the article to use react-basic-hooks throughout and use some conveniences that @andys8 put together with craco-purescript-loader in his create-react-app-purescript project. I also thought it would be nice to link to purescript-react-realworld in the article to show PureScript in a more realistic setting.

The updated post is up on the site:
https://thomashoneyman.com/articles/replace-react-components-with-purescript

24 Likes

This is really cool, and thank you for writing it.

I have been trying to incorporate some PureScript code to an existing TypeScript project with mixed success (some weirdness on either side of the code.)

My base setup is similar. Sharing src folder, symlinking output into src etc. But now I’ll go and experiment with creating interop code similar to yours to reduce/eliminate weirdness.

I have a question – any recommendations on representing more complex sum types? Something like:

data Action
  = Increment
  | Decrement
  | SetTo Int

(Perhaps the answer will be in “part 3” :smiley:)

(BTW, I’m using react-basic-hooks, but had to fork it because the current official impementation fails “rules of Hooks” on the TS side. If folks are interested, I can share my solution.)

For interop with TypeScript, purescript-variant works pretty well. @justinw has written quite a bit on the subject. https://github.com/justinwoo/purescript-ohyes

4 Likes

@mugatti Which implementation and rules are you referring to? What breaks?

@natefaubion, thanks! I’ll take a look at purescript-variant. BTW, I have been influenced heavily by @justinw’s posts and libraries in this effort. I’m generating my TypeScript declarations for product types basically in a very similar way to his, but I was stuck with the sum types.

@spicydonuts, I was talking about this issue. My setup is very similar to @thomashoneyman’s where I’m using compiled PureScript output as an input to my TypeScript/React build. I couldn’t find a way to override/edit the linter rules without “ejecting” from create-react-app which I didn’t want to do, so I forked the library for my own usage, and named all the hooks.

Edit: this is the change I made: https://github.com/muratg/purescript-react-basic-hooks/commit/ff755c67e6ed963a9aaf0500a6a41917df663322

It was a great article. Thank you! I went through all the examples, and to get the last one (i.e., Introducing More Complex Types) to compile, I had to change the following in Interop.purs:

import Counter (CounterType(…), Props, counter, counterTypeFromString)

becomes:

import Counter (CounterType(…), Props, component, initialState, render, counterTypeFromString)

Thus, I’m using the same imports and the same jsCounter code from the previous example (Making it usable from JavaScript).

2 Likes

@adkelley Thanks for catching this – it’s fixed on the site now :slight_smile:

@mugatti I’m glad you found it helpful!

With regards to the more complex sum types @natefaubion’s variant suggestion is a good one, as the output code is quite clear. For example, your data type, translated, might be:

type Action = Variant
  ( increment :: Unit
  , decrement :: Unit
  , setTo :: Int
  )

on the PureScript side; on the JavaScript side, your SetTo constructor would end up being an object like this:

{ type: "setTo", value: 10 }

which also lends itself to use on the JavaScript side with switch statements, like this:

processAction = action => {
  switch (action.type) {
    case "increment":
      ...
    
    case "setTo":
      console.log("count", action.value);
      ...
    
    default:
      console.log("didn't understand the action type")
  }

The next post in this series is about using Halogen components in JavaScript code bases and will get in-depth about doing this sort of thing to handle messages emitted by Halogen components. So you’ll have a bit more of a taste there!

Still, this isn’t a full answer, because you could be trying to represent all sorts of types in JavaScript. Some other approaches I’ve used include:

  1. Decide how I would have represented this in JavaScript and then translate backwards to PureScript – for example, maybe the component can take a string "increment" or "decrement", or an object with an action and a value like { "setTo": 10 }, and on the PureScript side I have to decode this. In that case I’d likely say that the JavaScript will provide a Foreign type with a comment and decode it manually (type Props = { counterType :: Foreign }).

  2. Decide that it’s not worth trying to make a good JavaScript representation of the type and instead export values directly for JavaScript to use. For example, I might export the values increment = Increment, decrement = Decrement, and setTo n = SetTo n and then import and use those values in JavaScript directly. Sometimes this is much easier than trying to do a translation.

I haven’t had to reach for anything more involved than what I’ve described here, fortunately, but I can imagine that others like @justinw have more advanced techniques.

2 Likes

Ah, I still don’t really understand that…

  1. It’s a linter rule, not a React rule (and technically the function is named correctly, just not in the style the linter wants)
  2. Linters should never run on generated code. No code generator could possibly make all linters happy with the same emitted code.

Any interest in adding a react-basic-hooks section? I’ve found interop to be even easier since you use the same paradigm for both PS and JS components, and if you end up needing to write a stateful component in an FFI file the hooks api lets you skip JS classes altogether (classes are extra tedious without the ES2016 class keyword).

3 Likes

I am interested in that, though I’ll have to learn about it first :slight_smile: I’d love to see how that works. Also I’m happy to link out to related articles if you’ve written anything on this topic!

1 Like

I tried to port your tutorial to the hooks API here (and attempted a custom hook in Form.purs): https://github.com/ptrfrncsmrph/purescript-react-tutorial
Would be interested in feedback as I’m new to PureScript and there’s maybe some language features I’m not taking advantage of.

2 Likes

How to get over the ‘module’ is not defined and ‘require’ is not defined errors while trying to use the generated js file?

Do you have any code or steps taken you can share? I didn’t encounter this error myself walking through the steps in the guide from scratch.

I’m a little surprised to see require is not defined, as that sounds like an issue with JavaScript ES6 modules. The article uses the import syntax import React from "react"; which works fine. I found this issue on the create-react-app repository which may match the issue you’re seeing.

However, if you are just going through the article as-is, then I don’t know why you’re encountering that error.

So if I generate a component in the output folder and use that as a replacement in the component where the JS version was used(which uses ES6) I get errors involving module and require

PureScript generates CommonJS modules, which are imported with require by default. However, build tools like Webpack and Parcel make it possible to use the more common import statement instead. The article uses Webpack so this is possible, but if you aren’t using one of these tools then I believe you have to import with require.

See:

as well as these issues on the PureScript compiler:

I’ve only used projects with tools like Webpack and Parcel, though, so I don’t have firsthand experience of how to do things without them. If you are using one of these, then I’m not quite sure what the issue is.

I ran into this issue too when launching a react project that bypassed create-react-app (so no webpack setup automatically). To follow-up on the previous post, here’s a parcel command that resolves this require error:

parcel serve assets/index.html -o index--parcelified.html --open

This assumes assets/index.html points to the js script generated from your purescript code.

@thomashoneyman Is your article hosted somewhere that takes PRs? I noticed some inconsistencies when I went through the tutorial. Also, is the final result tracked? Linking to something like @ptrfrncsmrph’s react-hooks port would be a helpful fallback reference.

@milesfrain My writing is on GitHub but in a private repository but I really ought to move the writing out to its own repository which can accept pull requests. I simply haven’t gotten around to it. And that’s a good idea to link to the react-hooks port, doing that now (Edit: updated on the site).

In the meantime, feel free to post article edits here or to send them to me (my email is on my site at https://thomashoneyman.com/open-invitation).

Thanks for the link! This was the first thing I attempted in PureScript, so for my part I’ve tried to polish it up with a few things I’ve learned since, but I’ve really only dabbled in the language. If there is anything in there that should be corrected, please let me know!

Seems good to me, though I usually inline render functions like renderCounter. Typing out the hooks type can be tedious and usually isn’t necessary.

1 Like