Introducing Halogen Hooks

I’m proud to announce today that I’ve released purescript-halogen-hooks, an implementation of Hooks for Halogen!

I’ve also written a short but comprehensive introducing to Hooks: why they’re useful, how they work, and how they can make your Halogen applications better:

https://thomashoneyman.com/articles/introducing-halogen-hooks

Hooks are an alternate approach to stateful code in Halogen that make it much easier to reuse code among components. They also happen to be a more convenient way to define components in general.

One thing that’s especially cool about Halogen is that a feature like this can be implemented entirely as an external library. PureScript and Halogen are both so powerful with such small cores that libraries like this are downright pleasant to implement. There are no changes to the underlying Halogen library.

If you use Halogen, I hope you try out Hooks and see how they feel in your application!

21 Likes

I also would like to (briefly) thank several people that put a lot of effort into making the library happen.

@natefaubion wrote a React Hooks implementation in our codebase at work. When I compared it to the renderless components I’ve written in Halogen (Select and Formless) it was an obvious improvement and I wanted them for Halogen, too. Nate came up with the original idea to write a DSL for Hooks in Halogen and guided a lot of the implementation.

@garyb dug me out of several thorny problems around managing queries and wrote an indexed free monad implementation I could use to track which Hooks were used after other Hooks. @davez and @JordanMartinez had great feedback that made the library more usable.


On an unrelated note, I don’t actually know how to publish this library now that the Bower registry is gone. I’d like to make it available for PureScript users in future package sets without having to manually add it to their packages.dhall file in Spago and I’d like to publish the documentation to Pursuit. If anyone knows the right links to check out that would guide me in doing this I’d appreciate it! (@f-f)

2 Likes

Congrats on the release! :slightly_smiling_face:

To answer your question: add your library to this list, then proceed as usual with pulp version and pulp publish.
The instructions to publish on package-sets have been updated too.
(Note: this setup is temporary)

Cool! I look forward to trying it out

1 Like

The hooks api itself is so similar to react-basic-hooks it makes me wonder if they could share some type definitions, allowing both libraries to share the same logic-only hooks… :thinking:

1 Like

I’m curious – do you mind elaborating on this? I see some similar types, like the foreign-imported UseRef and UseState types, and the Hook types are similar, but the corresponding hooks like useRef and useState are quite different and return different things. I’m not sure what would be gained if only these types were being shared, especially if they diverge at all over time. I may be misunderstanding what type definitions you’re thinking of, though, so I’d love to hear more!

Sure! It’s a very half-baked thought, but react-basic-hooks’ types and functions are only really defined the way they are because of the names used by React itself. If the same functionality is captured by the halogen-hooks api, maybe an abstracted useState could return a data structure like data HookImpl = UseState initialValue | ...other primitive constructors which each library interprets differently. You could then write a useWindowWidth hook which doesn’t care if it’s embedded in a halogen or react component.

Actually, that reminds me – last year I worked on a project called purescript-agnostic which tried to let you write the logic for components without being tied to React or Halogen (or Concur, or…), and then express the same logic through either library. Write the logic once, use it with any UI library kind of thing. But ultimately it felt like too much work to get around differences between the libraries.

Hooks, though, bring the two libraries closer together. Maybe with some shared types it really would become possible to write something like useWindowWidth that doesn’t care if it’s backed by Halogen or React, and only Hooks that render something have to care because of differences between JSX and Halogen’s ComponentHTML.

On the other hand, react-basic-hooks does use hook types that I didn’t find being a good fit for Halogen (useContext or useCallback, for example), and halogen-hooks requires types React wouldn’t care about (useQuery, for example). I’m not sure whether those could be reconciled.

But this has definitely got me thinking…

2 Likes

That’s true! Perhaps a typeclass could allow each implementation to extend the language, but that could also erode the benefits over time as well… ¯\_(ツ)_/¯

As for UI, I’ve wondered the same thing. I’ve been looking at elm-ui recently and I really like it. I’ve been thinking of making something similar for react-basic, with the hope of eventually adding react-native support as well. That api is actually closer to halogen than react-basic…

2 Likes

I’m also a big fan of that library, and an equivalent was one of the first things I looked for when migrating from Elm to PureScript.

2 Likes

I’m curious about useLifecycleEffect. What’s the use case for using it over useEffect?

Simplicity, mostly: I find the dependency array easy to get wrong and Halogen didn’t even have a way to run an effect after every render before. I think providing a way to run an effect for initialization without having to learn about that stuff is a win.

I also didn’t see a good way to have a single useEffect that covered all cases (only initialize / finalize; on every render unconditionally; on any render except for if the dependencies didn’t change). If I was going to have two effect hooks then I wanted one to handle the default case well.

Hmm maybe… it is the more traditional pattern but it kind of disconnects props from effects.

For the useEffect patterns:

  • only init/final: set a constant as the dep, like unit, behaves the same as useLifecycleEffect
    • the library could alias unit as once (or make it a new opaque type)
  • on every render: I’ve never seen a use case for this, but you could implement the dep’s Eq instance as const false
    • similarly, could export onEveryRender as a constant to be passed as the dep
  • on every render when no change: also never seen this, but you could create a NotEq newtype which defines Eq as not <<< eq
    • onNotChanged deps function

I’d like to end up with an easy-to-use useEffect function instead of two!

Just to clarify on the useEffect patterns, to make sure we’re on the same page:

  • only init/final: this corresponds to useEffect(fn, []), so providing an empty deps array means the effect is run once on init and the returned cleanup function once at unmount
  • on every render: this corresponds to useEffect(fn), where no deps array is provided, and so the effect will be run after every render
  • on every render, skipping when deps haven’t changed: this corresponds to useEffectFn(fn, [ dep1, dep2 ])

To handle these cases, you’re suggesting:

  • Only init/final: useEffect unit $ do ...
  • Every render: useEffect ???? $ do ...
  • Every render, skipping when deps have not changed: useEffect ???? $ do ...?

I’m curious how you would type this function. Since deps can be heterogeneous, I require them provided in a record which is under the hood coerced into an opaque MemoValues type that can be compared for equality later with an equality function a -> a -> Boolean or an Eq instance which provides it.

Ah, I misunderstood the “when deps changed” bit.

  • Only init/final:
    • yep! or any constant, you could create a data type for it too so it’s more self-documenting (see onInitFinal), but I don’t think it’s a great idea to promote this use case – better to get the deps right, especially when building reusable hooks
    • yes, in React people do this with useEffect(..., []), though the react-basic-hooks impl ends up doing this: useEffect(..., [unit]), same thing
  • Every render:
    • see OnRender below
    • the React equivalent would be useEffect(...) (no deps arg)
  • Every render, skipping when deps have not changed:
    • this is just a normal useEffect with deps :slight_smile:

You could implement these like this:

data OnRender = OnRender

instance eqOnRender :: Eq OnRender where
  eq _ _ = false

data OnInitFinal = OnInitFinal

instance eqOnInitFinal :: Eq OnInitFinal where
  eq _ _ = true

-- usage
React.do
  useEffect OnRender do ...this effect always runs
  useEffect OnInitFinal do ...this is useLifecycleEffect

But these are both still kind of anti-patterns in the React hooks world.


For the implementation, yeah, that sounds similar to the react-basic-hooks implementation. The only constraint is that the value has an Eq instance and it uses a similar trick to memo the old value when eq returns true. That’s an artifact of React’s implementation though, as it always uses reference equality.

One dep: useEffect dep do ...
Two deps: useEffect (dep1 /\ dep2) do ...
A dep with no Eq instance, like a function: useEffect (valueDep /\ UnsafeReference fnDep) do ...

Btw, I really like your suggestion of moving the hook bodies to the where clause to avoid accidental deps! When I (finally :sob:) get around to writing a react-basic-hooks guide I’m gonna steal that :slight_smile:

My personal opinion is that I don’t see a material difference between:

useEffect OnRender

and

useEffectOnRender

Since they both require learning about the existence of this special data type to drive functionality or a function with that name. I find the eq semantics a little strange (a /= a) for a data type like this. I know it’s meant to be an extension of the DSL, but that’s also what the function does.

2 Likes

I was thinking a cool way to do this to make migration as easy as possible for existing React JS users is to mirror the official JS Hooks guide (source), but replace the code snippets with PS. I believe the site license is permissive enough to allow this. Probably need to maintain the FB copyright notice though.

That’s not a bad idea…

Sounds great :smiley: I’d like to do that as a part 1, and then an “app” guide as part two (routing, data fetching) (assuming you’re talking about react-basic-hooks, since that’s the quote and the halogen-hooks guide linked above is there already :slight_smile:)

Edit: sorry, I guess this doesn’t really go in this thread – feel free to DM me if you want to discuss it more though