purescript-react / purescript-react-basic-hooks

An implementation of React hooks on top of purescript-react-basic
https://pursuit.purescript.org/packages/purescript-react-basic-hooks/
Apache License 2.0
198 stars 31 forks source link

restrictions on hooks captured by indexed monads #42

Closed jamesdbrock closed 3 years ago

jamesdbrock commented 3 years ago

Indexed monads seem to be a useful tool for capturing the restrictions on hooks, like in purescript-react-basic-hooks.

https://twitter.com/paf31/status/1197185171176341504

working on a pure model for React hooks based on indexed monads, inspired by purescript-react-hooks.

https://twitter.com/paf31/status/1231614635754819584

The screenshot is fairly useless right now, but I'm going to polish this up and turn it into a blog post when I have something a bit more complete.

https://twitter.com/paf31/status/1231614926503993345

I sure wish I could find that blog post. I don't think it exists?

I had a conversation last week which I will paraphrase like this:

Me: Why does react-basic-hooks need indexed monads?

@robertdp : Because React needs certain operations to be called in a certain order, and the indexed Render monad enforces that ordering at the type level.

Which makes sense, and it also seems to be what Phil is describing on Twitter. But I would like to understand it better. Which React “restrictions on hooks”, exactly, are captured by Render?

robertdp commented 3 years ago

It's not exactly that React needs certain operations to be called in a certain order. It's more that React uses the order in which hooks are run as a way of tracking them, and associates internal React state with each hook. This means that if the order of hook evaluation changes in a component then the behaviour is undefined and likely highly unsafe (unless React provides some guarantees about this, but I don't think they do).

Render is just an indexed monad that represents Effects, but the restrictions are modelled by Hook:

type Hook (newHook :: Type -> Type) a
  = forall hooks. Render hooks (newHook hooks) a

Like Phil was using tuples to build up the sequence of hooks, react-basic-hooks uses newtypes and Render:

customHook :: forall hooks. Render hooks (UseEffect Number (UseMemo Boolean Int (UseState String hooks))) Unit
customHook = React.do
  _ <- React.useState "hi"
  _ <- React.useMemo false \_ -> 1
  React.useEffect 22.2 mempty
  pure unit
jamesdbrock commented 3 years ago

Thank you @robertdp .

Here's the reference to the rule which is enforced by the Hook type. This is why we need the indexed monad Render.

https://reactjs.org/docs/hooks-rules.html

React relies on the order in which Hooks are called.

ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

megamaddu commented 3 years ago

Yep -- implementing React hooks with plain monads would still allow broken hook rules. Using indexed monads captures the same rules in the type system that eslint-plugin-react-hooks does in JS, except dependency list tracking.

jamesdbrock commented 3 years ago

And also, for those following along, react-basic-hooks recommends the “qualified-do” syntax because then we can write do blocks with the indexed monad Render.

https://pursuit.purescript.org/packages/purescript-react-basic-hooks/docs/React.Basic.Hooks#v:bind

https://pursuit.purescript.org/packages/purescript-indexed-monad/docs/Control.Monad.Indexed.Qualified