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.
Sections
Together, we will:
- Write a tiny React application in JavaScript
- Update the application to support PureScript
- Replace a React component with PureScript React, with the same interface and behavior as the original
- 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 (
<div>
<h1>My App</h1>
</div>
);
}
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) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
{this.props.label}: {this.state.count}
</button>
);
}
}
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 (
<div>
<h1>My App</h1>
+ <Counter label="Count" />
+ <Counter label="Clicks" />
+ <Counter label="Interactions" />
</div>
);
}
With yarn start
we can run the dev server and see our app in action.
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 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+
+# purescript
+output
+.psc*
+.purs*
+.spago
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:
src
├── 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:
- Write the component in idiomatic PureScript.
- Write a separate interop module for the component. This module provides the JavaScript interface and conversion functions between PureScript and JavaScript types and idioms.
- Use the PureScript compiler to generate JavaScript
- 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
let
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 ]
pure
{ 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 =
div'
[ 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?
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:
- Provides a mapping between PureScript types used in the component and a simple JavaScript representation
- Adds a layer of safety by marking all inputs that could reasonably be
null
orundefined
with theNullable
type, which helps our code handle missing values gracefully - 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
- 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:
- A new JavaScript-compatible interface (
JSProps
) - A function converting from the new types to PureScript types (
jsPropsToProps
) - 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:
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 }
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 ]
}
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:
- An effectful callback function to run after the counter is clicked
- 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 =
R.button
{ 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) {
console.log("clicked!");
}
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
...do 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 (
<div>
<h1>My App</h1>
- <Counter />
- <Counter label="Clicks:" />
+ <Counter onClick={n => console.log("clicked: ", n)} />
+ <Counter counterType="decrement" label="Clicks:" />
<Counter label="Interactions:" />
</div>
);
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.
Top comments (1)
Thanks For Sharing!
Read More: Top 10 Most Popular React Component Libraries and Frameworks in 2023