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!
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)
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.
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…
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.
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.
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
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
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
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 ...
My personal opinion is that I don’t see a material difference between:
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.
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.
Sounds great 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 )
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