DEV Community


How to Replace React Components With PureScript

thomashoneyman profile image Thomas Honeyman Updated on ・18 min read

I have twice experienced replacing large JavaScript apps with PureScript: first at CitizenNet, where we replaced Angular with Halogen, and then at Awake Security, where we've replaced most of a React app with PureScript React. Both companies saw a precipitous drop in bugs in their software.

The best way to rewrite any significant app from one language to another is incrementally, bit by bit, while it continues to run. At first the new language can simply take over logically separate parts of the app: the management dashboard, or the chat window, or a big form. But you will eventually want to intermix the languages: an autocomplete written in PureScript but used in a JavaScript form, or a PureScript component being passed a mix of components from both languages as children, or a shared global state.

Original: Replace React Components With PureScript's React Libraries

At this point the new language must be flexible enough to mix code from both languages together, not just take over a section of the app for itself. Fortunately, you can transform the interface of idiomatic PureScript into idiomatic JavaScript (and vice versa). Components written with any leading PureScript UI library can be interleaved with components written in JavaScript frameworks like Angular and React.

It's relatively easy to replace React apps with PureScript due to its react and react-basic libraries. Using the same underlying framework means the same idioms apply and components can be shared with little to no modification. We can share more than isolated components, too; at Awake Security we share internationalization, a Redux store and middleware, and other global context in an intermixed codebase where PureScript regularly imports JavaScript and JavaScript regularly imports PureScript.

In this article I will demonstrate how to replace part of a React application with simple components written in PureScript. Along the way, I'll share best practices for making this interop convenient and dependable. The examples will be simple, but the same techniques also apply to complex components.


Together, we will:

  1. Write a tiny React application in JavaScript
  2. Update the application to support PureScript
  3. Replace a React component with PureScript React, with the same interface and behavior as the original
  4. Replace the component again with React Basic

I encourage you to code along with this article; no code is omitted and dependencies are pinned to help ensure the examples are reproducible. This code uses Node v11.1.0, Yarn v1.12.0, and NPX v6.5.0 installed globally, and PureScript tooling installed locally. You can also view the original purescript react article.

Let's write a React app in JavaScript

We are going to write a tiny React application which shows a few counters, and then we're going to replace its components with PureScript. The resulting JavaScript code will be indistinguishable, aside from imports, from the original, and yet it will all be PureScript under the hood.

Let's follow the official React docs in using create-react-app to initialize the project and then trim our source code to the bare minimum.

# Create the app
npx create-react-app my-app && cd my-app

At the time of writing, create-react-app produces these React dependencies:

"dependencies": {
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"

We have a handful of source files under src, but our application will need just two of them: index.js, the entrypoint for Webpack, and App.js, the root component of our application. We can delete the rest:

# Delete all the source files except for the entrypoint and
# root app component
find src -type f -not \( -name 'index.js' -or -name 'App.js' \) -delete

Finally, let's replace the contents of those two files with the bare minimum we'll need for this article. From here on out I'll supply diffs that you can supply to git apply to apply the same changes I did.

First, our entrypoint:

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

Then our main app component:

// src/App.js
import React from "react";

function App() {
  return (
      <h1>My App</h1>

export default App;

Writing a React component

Let's write our first React component: a counter. This is likely the first example of a React component you ever encountered; it's the first example in the PureScript React libraries as well. It's also small and simple enough to be replaced twice over the course of this article.

touch src/Counter.js

The counter will be a button which maintains the number of times it has been clicked. It will accept, as its only prop, a label to display on the button.

// src/Counter.js
import React from "react";

class Counter extends React.Component {
  constructor(props) {
    this.state = {
      count: 0

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        {this.props.label}: {this.state.count}

export default Counter;

Then, we'll import our new counters into our main application:

--- a/src/App.js
+++ b/src/App.js
@@ -1,9 +1,13 @@
 import React from "react";
+import Counter from "./Counter";

 function App() {
   return (
       <h1>My App</h1>
+      <Counter label="Count" />
+      <Counter label="Clicks" />
+      <Counter label="Interactions" />

With yarn start we can run the dev server and see our app in action.

running app

Setting up a shared PureScript & JavaScript project

We've written entirely too much JavaScript. Let's support PureScript in this project as well. Our goal is to write code in either language and freely import in either direction without friction. To accomplish that, we will install PureScript tooling, create a separate PureScript source directory, and rely on the compiler to generate JavaScript code.

1. Install the compiler and package manager

First we must install PureScript tooling. I recommend using Yarn to install local versions of the compiler and Spago (a package manager and build tool) which match those used in this article. I'll use NPX to ensure all commands are run using local copies of this software.

# Install the compiler and the Spago package manager
yarn add -D purescript@0.13.0 spago@0.8.4

2. Initialize the project and package set

We can create a new PureScript project with spago init. As of version 0.8.4, Spago always initializes with the same package set, which means you should have identical package versions to those used to write this article. I'm using the psc-0.13.0-20190607 package set.

# npx ensures we're using our local copy of Spago installed in node_modules.
npx spago init

Spago has created a packages.dhall file which points at the set of packages which can be installed and a spago.dhall file which lists the packages we've actually installed. We can now install any dependencies we need and we'll know for sure the versions are all compatible.

Before installing anything, let's update the existing .gitignore file to cover PureScript. For a Spago-based project this will work:

--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,9 @@
+# purescript

3. Adjust the directory structure

Finally, let's organize our source code. It's typical to separate JavaScript source from PureScript source except when writing an FFI file for PureScript. Since we aren't doing that in this project, our source files will be entirely separated. Let's move all JavaScript code into a javascript subdirectory and create a new purescript folder next to it.

mkdir src/javascript src/purescript
mv src/App.js src/Counter.js src/javascript

Next, we'll adjust index.js to the new location of our root component:

--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,5 @@
 import React from "react";
 import ReactDOM from "react-dom";
-import App from "./App";
+import App from "./javascript/App";

 ReactDOM.render(<App />, document.getElementById("root"));

We've just one task left. The PureScript compiler generates JavaScript into a directory named output in the root of the project. But create-react-app disables importing anything outside the src directory. While there are fancier solutions, for this project we'll get around the restriction by symlinking the output directory into the src directory.

# we can now import compiled PureScript from src/output/...
ln -s $PWD/output $PWD/src

Your src directory should now look like this:

├── index.js
├── javascript
│ ├── App.js
│ └── Counter.js
├── output -> ../output
└── purescript

Replacing a React component with PureScript React

I like to follow four simple steps when replacing a JavaScript React component with a PureScript one:

  1. Write the component in idiomatic PureScript.
  2. Write a separate interop module for the component. This module provides the JavaScript interface and conversion functions between PureScript and JavaScript types and idioms.
  3. Use the PureScript compiler to generate JavaScript
  4. Import the resulting code as if it were a regular JavaScript React component.

We'll start with the react library, which we use at Awake Security. It's similar to react-basic but maps more directly to the underlying React code and is less opinionated. Later, we'll switch to react-basic, which will demonstrate some differences between them.

As we take each step in this process I'll explain more about why it's necessary and some best practices to keep in mind. Let's start: install the react library and prepare to write our component:

# install the purescript-react library
npx spago install react

# build the project so editors can pick up the `output` directory
npx spago build

# create the component source file
touch src/purescript/Counter.purs

1. Write the React component in idiomatic PureScript

Even though we are writing a component to be used from JavaScript, we should still write ordinary PureScript. As we'll soon see, it's possible to adjust only the interface of the component for JavaScript but leave the internals untouched. This is especially important if this component is meant to be used by both PureScript and JavaScript; we don't want to introduce any interop-related awkwardness to either code base.

Below, I've written a version of the component with the same props, state, and rendering. Copy its contents into src/purescript/Counter.purs.

Note: It's not necessary to annotate this when creating a component, but doing so improves the quality of errors if you do something wrong.

module Counter where

import Prelude

import React (ReactClass, ReactElement, ReactThis, component, createLeafElement, getProps, getState, setState)
import React.DOM as D
import React.DOM.Props as P

type Props = { label :: String }

type State = { count :: Int }

counter :: Props -> ReactElement
counter = createLeafElement counterClass

counterClass :: ReactClass Props
counterClass = component "Counter" \(this :: ReactThis Props State) -> do
    render = do
      state <- getState this
      props <- getProps this
      pure $ D.button
        [ P.onClick \_ -> setState this { count: state.count + 1 } ]
        [ D.text $ props.label <> ": " <> show state.count ]

    { state: { count: 0 }
    , render

In a PureScript codebase this is all we need; we could use this component by importing counter and providing it with its props:

-- compare to our JavaScript main app
import Counter (counter)

renderApp :: ReactElement
renderApp =
    [ h1' [ text "My App" ]
    , counter { label: "Count" }
    , counter { label: "Count" }
    , counter { label: "Count" }

We can already use this component from JavaScript, too. The react library will generate a usable React component from this code which we can import like any other JavaScript React component. Let's go ahead and try it out, and then we'll make a few improvements.

First, we'll compile the project:

npx spago build

Then we'll import the component. Note how our implementation is close enough that we only have to change the import, nothing else! PureScript will generate files in output, so our counter component now resides at output/Counter.

--- a/src/javascript/App.js
+++ b/src/javascript/App.js
@@ -1,5 +1,5 @@
 import React from "react";
-import Counter from "./Counter";
+import { counter as Counter } from "../output/Counter";

 function App() {
   return (

Run yarn start and you ought to see the exact same set of counters as before. With our component now implemented in PureScript we don't need our JavaScript version anymore:

rm src/javascript/Counter.js

We've successfully taken over part of our JavaScript app with PureScript.

2. Write an interop module for the component

We were lucky our component worked right away. In fact, it only worked because we're using simple JavaScript types so far and the users of our counter component are trustworthy and haven't omitted the label prop, which we consider required. We can enforce correct types and no missing values in PureScript, but not in JavaScript.

What happens if a user forgets to provide a label to the component?

forgotten label prop

Well, setting undefined as a label is not good, but it's not as bad as the entire app crashing -- which is what happens if you attempt to use PureScript functions on the value you have pretended is a String. The problem is that the String type doesn't quite capture what values are likely to arrive from JavaScript. As a general rule, I expect people to write JavaScript they way they usually do, which means using builtin types, regular uncurried functions, and to sometimes omit information and supply null or undefined instead. That's why at Awake Security we usually provide an interop module for components that will be used in JavaScript code, which:

  1. Provides a mapping between PureScript types used in the component and a simple JavaScript representation
  2. Adds a layer of safety by marking all inputs that could reasonably be null or undefined with the Nullable type, which helps our code handle missing values gracefully
  3. Translates functions in their curried form into usual JavaScript functions, and translates effectful functions (represented as thunks in generated code) into functions that run immediately when called
  4. Serves as a canary for changes in PureScript code that will affect dependent JavaScript code so that you can be extra careful

Over the remainder of the article we'll explore each of these techniques. For now, all we need to do is mark the input string as Nullable and explicitly handle what should happen when it is omitted.

Let's create an interop module for our component named Counter.Interop:

mkdir src/purescript/Counter
touch src/purescript/Counter/Interop.purs

Typically, each interop module will contain at least three things:

  1. A new JavaScript-compatible interface (JSProps)
  2. A function converting from the new types to PureScript types (jsPropsToProps)
  3. A new component which uses the new JavaScript-compatible types via the conversion function (jsComponentName)

In action:

module Counter.Interop where

import Prelude

import Counter (Props, counter)
import Data.Maybe (fromMaybe)
import Data.Nullable (Nullable, toMaybe)
import React (ReactElement)

type JSProps = { label :: Nullable String }

jsPropsToProps :: JSProps -> Props
jsPropsToProps { label } = { label: fromMaybe "Count" $ toMaybe label }

jsCounter :: JSProps -> ReactElement
jsCounter = counter <<< jsPropsToProps

We have created a new interface for our component, JSProps, which will be used in JavaScript instead of our PureScript interface, Props. We've also created a function which translates between the two interfaces, and produced a new component which uses the JavaScript interface instead of the PureScript one.

Marking the label prop as Nullable makes the compiler aware the string may not exist. It then forces us to explicitly handle the null or undefined case before we can treat the prop as a usual String. We'll need to handle the null case in order to map our new JSProps type to our component's expected Props type. To do that, we convert Nullable to Maybe and then provide a fallback value to use when the prop does not exist.

The Nullable type is explicitly for interop with JavaScript, but it doesn't always behave exactly the way you would expect. It does not map directly to the ordinary Maybe type. You should usually convert any Nullable types to Maybe as soon as possible. Check out the nullable library if you'd like to learn more about this.

Let's change the import in App.js and verify that the omitted label is handled gracefully.

--- a/src/javascript/App.js
+++ b/src/javascript/App.js
@@ -1,5 +1,5 @@
 import React from "react";
-import { counter as Counter } from "../output/Counter";
+import { jsCounter as Counter } from "../output/Counter.Interop";

 function App() {
   return (

Now, omitted props still render a reasonable label:

forgot props fixed

In this case our interop module simply marked a single field as Nullable. But it's common for the JavaScript interface to diverge slightly from the PureScript interface it is translating. Keeping a separate interop module makes it easy to do this without affecting the core component.

It also ensures that any changes to the underlying component are reflected as type errors in the interop file rather than (potentially) silently breaking JavaScript code. It's easy to become lazy about this when you're used to the compiler warning you of the effect changes in one file will have in another!

If you use TypeScript, Justin Woo has written a piece on transparently sharing types with Typescript from PureScript which is worth reading.

Replacing a React component with PureScript React Basic

Let's try replacing the counter again, but this time with the newer, more opinionated react-basic library. Along the way we'll use a few more complex types and build a more sophisticated interop module.

Install react-basic:

npx spago install react-basic

Next, replace the contents of Counter with an identical implementation written with react-basic:

module Counter where

import Prelude

import React.Basic (JSX, createComponent, make)
import React.Basic.DOM as R
import React.Basic.DOM.Events (capture_)

type Props = { label :: String }

counter :: Props -> JSX
counter = make (createComponent "Counter") { initialState, render }
  initialState = { count: 0 }

  render self =
      { onClick:
          capture_ $ self.setState \s -> s { count = s.count + 1 }
      , children:
          [ R.text $ self.props.label <> " " <> show self.state.count ]

The two React libraries don't share types, so we'll change our interop module to describe producing JSX rather than a ReactElement.

--- a/src/purescript/Counter/Interop.purs
+++ b/src/purescript/Counter/Interop.purs
@@ -5,13 +5,13 @@ import Prelude
 import Counter (Props, counter)
 import Data.Maybe (fromMaybe)
 import Data.Nullable (Nullable, toMaybe)
-import React (ReactElement)
+import React.Basic (JSX)

 type JSProps = { label :: Nullable String }

 jsPropsToProps :: JSProps -> Props
 jsPropsToProps { label } = { label: fromMaybe "Count" $ toMaybe label }

-jsCounter :: JSProps -> ReactElement
+jsCounter :: JSProps -> JSX
 jsCounter = counter <<< jsPropsToProps

Making it usable from JavaScript

This component works perfectly well in a PureScript codebase. Unlike our react component, though, our react-basic component will not automatically work in JavaScript code also. Instead, we need to use make to construct a component meant for PureScript and toReactComponent to construct one for JavaScript.

Still, both functions use the same component spec type, so the new restriction is easy to work around. We'll simply move initialState and render out to the module scope. That way we can import them directly into our interop module to supply to toReactComponent.

--- a/src/purescript/Counter.purs
+++ b/src/purescript/Counter.purs
@@ -2,21 +2,28 @@ module Counter where

 import Prelude

-import React.Basic (JSX, createComponent, make)
+import React.Basic (Component, JSX, Self, createComponent, make)
 import React.Basic.DOM as R
 import React.Basic.DOM.Events (capture_)

 type Props = { label :: String }

+type State = { count :: Int }
+component :: Component Props
+component = createComponent "Counter"
 counter :: Props -> JSX
-counter = make (createComponent "Counter") { initialState, render }
-  where
-  initialState = { count: 0 }
-  render self =
-    R.button
-      { onClick:
-          capture_ $ self.setState \s -> s { count = s.count + 1 }
-      , children:
-          [ R.text $ self.props.label <> " " <> show self.state.count ]
-      }
+counter = make component { initialState, render }
+initialState :: State
+initialState = { count: 0 }
+render :: Self Props State -> JSX
+render self =
+  R.button
+    { onClick:
+        capture_ $ self.setState \s -> s { count = s.count + 1 }
+    , children:
+        [ R.text $ self.props.label <> " " <> show self.state.count ]
+    }

We'll leave the code otherwise unchanged. Next, let's turn to the interop module. It should now use toReactComponent to create a component usable from JavaScript. This function takes the component and component spec, exactly the same way that make does, but it also takes an additional argument: our jsPropsToProps function.

The react-basic library makes interop more explicit than react does, but ultimately we'll write nearly the same interop code.

--- a/src/purescript/Counter/Interop.purs
+++ b/src/purescript/Counter/Interop.purs
@@ -2,16 +2,15 @@ module Counter.Interop where

 import Prelude

-import Counter (Props, counter)
+import Counter (Props, component, initialState, render)
 import Data.Maybe (fromMaybe)
 import Data.Nullable (Nullable, toMaybe)
-import React (ReactElement)
-import React.Basic (JSX)
+import React.Basic (ReactComponent, toReactComponent)

 type JSProps = { label :: Nullable String }

 jsPropsToProps :: JSProps -> Props
 jsPropsToProps props = { label: fromMaybe "Count:" $ toMaybe props.label }

-jsCounter :: JSProps -> JSX
-jsCounter = counter <<< jsPropsToProps
+jsCounter :: ReactComponent JSProps
+jsCounter = toReactComponent jsPropsToProps component { initialState, render }

This component is now once again usable from JavaScript.

Introducing more complex types

What happens when you have a more complicated type you need to construct from JavaScript? Let's say, for example, that our counter component needs two new pieces of information:

  1. An effectful callback function to run after the counter is clicked
  2. A type which represents whether the function should increment or decrement on click

We can apply the same process to accommodate the new features. We will write idiomatic PureScript in our component module and then write a translation in the interop module. The end result will be a component equally usable in PureScript code or in JavaScript code, without compromising how you write code in either language.

--- a/src/purescript/Counter.purs
+++ b/src/purescript/Counter.purs
@@ -2,14 +2,35 @@ module Counter where

 import Prelude

-import React.Basic (Component, JSX, Self, createComponent, make)
+import Data.Maybe (Maybe(..))
+import Effect (Effect)
+import React.Basic (Component, JSX, Self, createComponent, make, readProps, readState)
 import React.Basic.DOM as R
 import React.Basic.DOM.Events (capture_)

-type Props = { label :: String }
+type Props =
+  { label :: String
+  , onClick :: Int -> Effect Unit
+  , counterType :: CounterType
+  }

 type State = { count :: Int }

+data CounterType
+  = Increment
+  | Decrement
+counterTypeToString :: CounterType -> String
+counterTypeToString = case _ of
+  Increment -> "increment"
+  Decrement -> "decrement"
+counterTypeFromString :: String -> Maybe CounterType
+counterTypeFromString = case _ of
+  "increment" -> Just Increment
+  "decrement" -> Just Decrement
+  _ -> Nothing
 component :: Component Props
 component = createComponent "Counter"

@@ -23,7 +44,15 @@ render :: Self Props State -> JSX
 render self =
     { onClick:
-        capture_ $ self.setState \s -> s { count = s.count + 1 }
+        capture_ do
+          state <- readState self
+          props <- readProps self
+          let
+            newCount = case props.counterType of
+              Increment -> add state.count 1
+              Decrement -> sub state.count 1
+          self.setState _ { count = newCount }
+          props.onClick newCount
     , children:
         [ R.text $ self.props.label <> " " <> show self.state.count ]

With these changes our counter can decrement or increment and can run an arbitrary effectful function after the click event occurs. But we can't run this from JavaScript: there is no such thing as a CounterType in JavaScript, and a normal JavaScript function like...

function onClick(ev) {

will not work if supplied as the callback function. It's up to our interop module to smooth things out.

I'll make the code changes first and describe them afterwards:

--- a/src/purescript/Counter/Interop.purs
+++ b/src/purescript/Counter/Interop.purs
@@ -2,16 +2,27 @@ module Counter.Interop where

 import Prelude

-import Counter (Props, counter)
+import Counter (CounterType(..), Props, component, initialState, render, counterTypeFromString)
 import Data.Maybe (fromMaybe)
 import Data.Nullable (Nullable, toMaybe)
+import Effect.Uncurried (EffectFn1, runEffectFn1)
 import React.Basic (JSX)

-type JSProps = { label :: Nullable String }
+type JSProps =
+  { label :: Nullable String
+  , onClick :: Nullable (EffectFn1 Int Unit)
+  , counterType :: Nullable String
+  }

 jsPropsToProps :: JSProps -> Props
-jsPropsToProps props = { label: fromMaybe "Count:" $ toMaybe props.label }
+jsPropsToProps props =
+  { label:
+      fromMaybe "Count:" $ toMaybe props.label
+  , onClick:
+      fromMaybe mempty $ map runEffectFn1 $ toMaybe props.onClick
+  , counterType:
+      fromMaybe Increment $ counterTypeFromString =<< toMaybe props.counterType
+  }

First, I updated the JavaScript interface to include the two new fields our component accepts.

I decided to represent CounterType as a lowercase string "increment" or "decrement" and guard against both the case in which the value isn't provided (Nullable) or the provided value doesn't make sense (it can't be parsed by counterTypeFromString). In either case the component will default to incrementing.

I also decided to represent onClick as a potentially missing value. But instead of a usual function, I'm representing the value as an EffectFn1: an effectful, uncurried function of one argument.

That type merits a little extra explanation. In PureScript, functions are curried by default and effectful functions are represented as a thunk. Therefore, these two PureScript functions:

add :: Int -> Int -> Int
log :: String -> Effect Unit not correspond to functions that can be called in JavaScript as add(a, b) or log(str). Instead, they translate more closely to:

// each function has only one argument, and multiple arguments are represented
// by nested functions of one argument each.
const add = a => b => a + b;

// effectful functions are thunked so they can be passed around and manipulated
// without being evaluated.
const log = str => () => console.log(str);

This is an unusual programming style for JavaScript. So PureScript provides helpers for exporting functions that feel more natural.

  • The Fn* family of functions handles pure functions of N arguments
  • The EffectFn* family of functions handles effectful functions of N arguments
  • A few other translating functions exist; for example, you can turn Aff asynchronous functions into JavaScript promises and vice versa.

If we rewrite our PureScript definitions to use these helpers:

add :: Fn2 Int Int Int
log :: EffectFn1 String Unit

then we will get a more usual JavaScript interface:

const add = (a, b) => a + b;
const log = str => console.log(str);

Without using EffectFn1, JavaScript code using our counter component would have to supply a thunked callback function like this:

<Counter onClick={count => () => console.log("clicked: ", n)} />

With EffectFn1 in place, however, we can provide usual code:

<Counter onClick={count => console.log("clicked: ", n)} />

Let's take advantage of our new component features by updating App.js. Our first component will omit all props except for the onClick callback, which will log the count to the console. The next one will specify a decrementing counter. The last component will stick to the original interface and just provide a label.

--- a/src/javascript/App.js
+++ b/src/javascript/App.js
@@ -5,8 +5,8 @@ function App() {
   return (
       <h1>My App</h1>
-      <Counter />
-      <Counter label="Clicks:" />
+      <Counter onClick={n => console.log("clicked: ", n)} />
+      <Counter counterType="decrement" label="Clicks:" />
       <Counter label="Interactions:" />

running app with decrement and function

Wrapping Up

We replaced a simple counter in this article, but the same steps apply to more complex components as well.

  • Write the PureScript component using whatever types and libraries you would like.
  • Then, write an interop module for the component which translates between JavaScript and PureScript.
  • Compile the result.
  • Import it into your JavaScript code like any other React component.

Interop between React and PureScript becomes more involved when you introduce global contexts like a Redux store, but it's bootstrapping work that remains largely out of view in day-to-day coding.

Interop between other frameworks like Angular or other PureScript UI libraries like Halogen is less transparent. That's not because of a limitation in these libraries, but simply because you're now mixing frameworks together. At CitizenNet we exported our Halogen components for Angular and React teams in the company to use.

The next time you're faced with a tangled JavaScript React app and wish you had better tools, try introducing PureScript.

Discussion (0)

Editor guide