How to write PureScript react components to replace JavaScript
thomashoneyman.com
thomashoneyman.com
export const Counter = ({label, counterType, onClick}) => {
const [counter, setCounter] = useState(0);
return (
<button onClick={() => {
counterType == "Increment" && setCounter(counter + 1);
counterType == "Decrement" && setCounter(counter - 1);
onClick && onClick();
}}>
{label}: {counter}
</button>
);
}
I think the case for PureScript React would be more convincing if the code you ended up with was loosely as concise as the above. (defn Counter [{:keys [label counterType onClick]
:or {onClick (fn [])}}]
(let [count (r/atom 0)]
[:button
{:onClick #(do (swap! count (condp = counterType
:increment inc
:decrement dec))
(onClick))}
(str label ": " @count)])))Here are some features off the top of my head (at least that's what they claim):
- Well-integrated stack, so very little friction
- “Datomic on the front end”: Client-side time-traveling database with automatic normalization, querying via EQL (kinda like GraphQL but more idiomatic and supposedly more powerful), “co-locating” queries in the UI.
- React bindings and a Semantic UI library
- Lets you swap anything you “outgrow”
- Lots of learning material (but still lacking in some areas from a cursory look). The author seems to be very active on Slack (Clojurians #fulcro).
- Fulcro RAD[2] (currently alpha) looks like it's going to reduce friction even further.
There's probably more cool stuff I don't remember now.
The two things I'm still insecure about with Clojure are its dynamically typed nature (but I'd be glad to hear about how that isn't a problem) and that the ecosystem doesn't seem to hold your hand too much (again, I'd like to hear another point of view).
I recommend using clj-kondo to catch the kinds of silly mistakes a type system is usually good for, yesterday we discovered that the react component interop is really good, we decided to try using https://chakra-ui.com/ over semantic ui and it's really straight forward to use React components that have no awareness of Fulcro or CLJS
I think in terms of hand holding Fulcro does a really good job once you get into the weeds because it has an opinion and documentation for many problems, if you want to join us we're #bristol-clojurians on slack
Sorry, which videos are you watching?
+ There's a signal to noise ratio that's off the charts
+ You've not cheated to achieve succinctness; there's no blackbox.nowDrawTheRestOfTheOwl()
- I don't think that's "pretty" code
- i don't find it easy to parse, i had to read your example with my index finger pressed against the screen as i traced through it - i didn't have to do that with the Typescript further example down-threadI found that learning to read the lisp-style indentation (which is purely by convention but has widespread acceptance and tooling support) was a pretty fast process with a solid payoff. It becomes easier to jump between levels of detail, and I now find it to be more readable than C-style syntax, even though I only write JS for work.
For example, in the code above, “what it does“ is made clear by reading downward without looking very far to the right: it draws a button with a click handler and a label. There is a guarantee offered by immutability that says “anything deeper than my current level cannot affect things outside of itself.”
That may sound like a lie, since the :onClick handler updates an atom, but knowing that a dereferenced atom (@count) has a stable value during the lifetime of the function call resolves it.
I don’t know how strong that guarantee is with other lisps; I think Clojure’s immutability plays a key role though.
If anything, the clojure syntax has fewer rules to understand - from my very limited knowledge of it.
It applies to React/Redux folks as well.
> There are benefits to having data in the one place:
> Here's the big one: because there is a single source of truth, we write no code to synchronise state between many different stateful components. I cannot stress enough how significant this is. You end up writing less code and an entire class of bugs is eliminated. (This mindset is very different to OO which involves distributing state across objects, and then ensuring that state is synchronised, all the while trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
> Because all app state is coalesced into one atom, it can be updated with a single reset!, which acts like a transactional commit. There is an instant in which the app goes from one state to the next, never a series of incremental steps which can leave the app in a temporarily inconsistent, intermediate state. Again, this simplicity causes a certain class of bugs or design problems to evaporate.
mkCounter :: Effect (ReactComponent {})
mkCounter = do
component "Counter" \props -> React.do
counter /\ setCounter <- useState 0
pure
$ R.button
{ onClick: handler_ do
setCounter (_ + 1)
, children:
[ R.text $ "Increment: " <> show counter ]
}
Your aesthetic / background may differ from mine, but I find the Purescript version easier to read.[1]: https://github.com/spicydonuts/purescript-react-basic-hooks
mkCounter :: String -> (Int -> Int) -> Effect (ReactComponent {})
mkCounter label op = do
component "Counter" \props -> React.do
counter /\ setCounter <- useState 0
pure
$ R.button
{ onClick: handler_ do
setCounter op
, children:
[ R.text $ label <> ": " <> show counter ]
}
now you can call mkCounter "Increment" (_ + 1)Although I can read Purescript reasonably well I'm not so proficient writing it (I do most of my programming in a different FP language, so I understand the concepts but not the specifics.) I didn't attempt to modify the code I pulled from the example because it would take more time than I have to install Purescript and check my modifications.
It would be simple to add this functionality. This equivalent of the `counterType` would be an algebraic data type, defined looking something like
data counterType = Increment | Decrement
and usage something like case counterType of
Increment -> ... make increment here ...
Decrement -> ... make decrement here ... (_ + 1)
Equivalent to this JS function: x => x + 1
Operator Sections: https://github.com/purescript/documentation/blob/master/lang... export const Counter: FunctionComponent<{
label: string;
counterType: 'Increment' | 'Decrement';
onClick: () => void
}> = ({ label, counterType, onClick }) => {
const [counter, setCounter] = useState<number>(0);
return (
<button onClick={() => {
counterType == "Increment" && setCounter(counter + 1);
counterType == "Decrement" && setCounter(counter - 1);
onClick && onClick();
}}>
{label}: {counter}
</button>
);
}You can add types if you want. And maybe one can discuss if there is a more elegant syntax than the C-style syntax of JavaScript (there is ...).
Other than that. I think the code is about as concise as it gets.
FP approaches to UI like Elm win the 100+ line comparisons for reasons you can't see, like minimizing runtime errors and making it so that impossible states are impossible at the type level.
Unfortunately it's hard to see the benefits until you get your hands dirty.
I use Elm in production on some apps and its a good place to start if you want simplicity. Personally, Purescript never interested me because I don't like all the complexity of more advanced Haskell features. While Elm specifically avoids certain advanced features to stay simple. You might like it given your concerns.
You'll probably feel the need for stronger typing at some point when adhering to these principles, and that's when I'd suggest looking at Typescript. By trying the things above you'll likely get familiar with the issues TS is trying to solve & you'll appreciate it more. Note that fp-ts is just a "helper" library for functional concepts, but it won't teach you their value or how to use them.
Also: many libraries have fp variations! Next time you reach for lodash, try lodash/fp instead: it has the same tools you'll already be familiar with, but implemented in a functional manner.
I think the counter updates should use functional updates, since they depend on the current state. My understanding is that this can lead to a potential race condition, although I'm not 100% on the details.
setCounter(currentCounter => currentCounter + 1)
https://reactjs.org/docs/hooks-reference.html#functional-upd...Interestingly your code is similar to some the first code in the basic 'into to hooks' section of the docs. Perhaps it's not a huge issue? Maybe somebody else who knows a bit more about React internals can chime in.
export const Counter = ({label, counterType, onClick}) => {
const [counter, setCounter] = useState(0);
onClick = onClick || () => {};
const handleClick = () => {
const modifier = counterType === "Increment" ? 1 : -1;
setCounter(counter + modifier);
onClick();
}
return (
<button onClick={handleClick}>
{label}: {counter}
</button>
);
}
There's another commenter that uses Typescript, but I believe PropTypes (https://www.npmjs.com/package/prop-types) are still viable as well - although it moves the checking from compile-time to runtime. I do believe VS Code and co can interpret PropTypes to provide in-editor hints though. And JSDoc as well.See my extensive post "A (Mostly) Complete Guide to React Rendering) for details:
https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...
const handleClick = useCallback(() => {
const modifier = counterType === "Increment" ? 1 : -1
setCounter(counter + modifier)
onClick()
}, [onClick, counter, counterType, modifier])
The array at the end is the list of "dependencies" — whenever one of them changes, the function is re-cached. Compiler checks can catch missing dependencies. You might think, "But that function will be redefined pretty much every time," and in this case that's true, and memoization may not help much. But in bigger trees of components, most redraws will be unrelated to any given element, so those dependencies will rarely change.useCallback() (and its twin useMemo(), for non-functional values) gets more useful in deeper component trees, where values may be passed down several levels.
The surface level ergonomics of JSC and the VDOM are great but good lord does it look painful at a second glance. I’m glad I took svelte for a spin.
[1]: https://github.com/spicydonuts/purescript-react-basic-hooks
Or for a more native PureScript approach to single-page web apps, check out purescript-halogen[2] and purescript-halogen-hooks[3] (created by the author of this article).
[1]: https://github.com/spicydonuts/purescript-react-basic-hooks
[2]: https://github.com/purescript-halogen/purescript-halogen
[3]: https://github.com/thomashoneyman/purescript-halogen-hooks
spago init
spago install halogen
spago build
Halogen is now at v5 which has removed a lot of the complexity and type wierdness that was around in v4. Highly recommend giving it another spin!Can't speak for how well it works on Windows though. Perhaps someone elso out there knows?
Does anyone have experience with this? How practical would it be to use this where one would otherwise use Go?
[1] https://discourse.purescript.org/t/purescript-native-can-now...
I haven't heard many news from its ecosystem since Phil Freeman stepped down from its active development. Purescript by Example must be quite outdated by now? I know that Alex Kelley is still working on his set of tutorials "Make the Leap from Javascript to Purescript"[0]. Anything else interesting going on?
I feel like there was a lot of excitement about Purescript in around 2015-2016, but then it sort of died out?
The Discourse ( https://discourse.purescript.org/ ) and Slack channels ( https://fpchat-invite.herokuapp.com/ ) are active (and quite welcoming / helpful).
A community fork of "PureScript by Example" is being updated / maintained, here: https://book.purescript.org/
Try PureScript is back online after a hiatus: https://try.purescript.org/
The compiler is now at version 0.13.8 with reasonably regular updates (and 0.14.0 should be on the way reasonably soon): https://github.com/purescript/purescript/releases
Spago, the new(ish) package manager, is quite nice: https://github.com/purescript/spago
The VSCode integration is solid. I'd say the tooling story is pretty good, over all.
Work is underway to provide a package registry, since bower is being phased out: https://github.com/purescript/registry
Halogen 5 was released recently: https://github.com/purescript-halogen/purescript-halogen
I suspect if they migrated from JavaScript to TypeScript instead they would have seen the same dramatic drop in bugs, but also migrated much quicker and would be able to hire engineers easier, and development speed would have increased (because of much better tooling/IDE/interop).
I think PureScript is a very bright project, I even want to try it myself, but realistically, comparison to plain JavaScript is not the best benchmark.
echo -e "\noutput\n.psc*\n.purs*\.spago" >> .gitignore
Sure, ok, saying "add these entries to your .gitignore" is too pedestrian? tee -a .gitignore <<'EOF'
output
.psc*
.purs*
.spago
EOF
(or use: cat <<'EOF' >> .gitignore
output
.psc*
.purs*
.spago
EOF
But I think the tee-version more readily emphasize what's going on, and works with sudo).Looking at the examples, the code seems to function the same so it's not obvious what problem is being solved that warrants teaching a whole dev team a new language.
In my experience, the vast majority of React app bugs are 1) state management, or 2) asynchronousisty. How does PureScript help to prevent those two classes of bugs?
Why would you choose to write a language who's type system is much less expressive than languages like Purescript? Why not have that expressive power and its ability to prevent some classes of bugs, and still have good interop with JS?
https://reasonml.github.io/reason-react/docs/en/components
https://reasonml.github.io/reason-react/docs/en/usestate-eve...
As far as I can tell the integration is cleaner and has better support from upstream? I guess it's not Haskell - but I really would be interested in why you would choose purescript for this use-case (personal preference is a fine reason, but I'd at least hope that preference goes beyond merely syntax?).
I'm happy to argue the benefits of either. The differences between it and Purescript are much smaller than the differences between them and most things.
There is a world outside the JS/TS/Node ecosystem, pick up some of these other languages and it will answer your question.