-
Notifications
You must be signed in to change notification settings - Fork 65
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial hooks implementation #159
base: main
Are you sure you want to change the base?
Conversation
useState | ||
:: forall a | ||
. a | ||
-> Hook (Tuple a (SetState a)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be interested to see if the state handling internally is fully parametric or if it still has all the sugar around copying/merging properties that component setState
uses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think one reason why this can't be parametric is that the setState
state function is overloaded to either take the state as is or a function, which means you can't have a anything represented by a function as state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is the relevant section handling the state:
https://github.com/facebook/react/blob/52bea95cfce4a75bc7c1a09455abaeef50fa0700/packages/react-reconciler/src/ReactFiberHooks.js#L333-L472
The only thing I see is that the code checks if the state is a function:
https://github.com/facebook/react/blob/52bea95cfce4a75bc7c1a09455abaeef50fa0700/packages/react-reconciler/src/ReactFiberHooks.js#L455-L457
As you mention it seems the overloading is problematic. I am open to ideas or suggests for the representation on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately there's no safe way to describe the API. One option would be to enumerate JS primitives. This is conservative.
foreign import data StateValue :: Type -> Type
intValue :: Int -> StateValue Int
intValue = unsafeCoerce
stringValue :: String -> StateValue String
stringValue = unsafeCoerce
recordValue :: forall r. { | r } -> StateValue { | r }
recordValue = unsafeCoerce
useState :: forall a. StateValue a -> Hook (Tuple a (SetState a))
This could potentially be overloaded with an instance chain that closes it off with a custom type error. You could then expose an unsafeUseState
function which documents the caveat that you can't use anything which has a function as it's representation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of enumerating the primitives, and maybe this is the best option. However, what you think about testing for a function and wrapping the passed in state in a function if detected. Maybe this is a bit too unsafe, but I think it might work.
src/React/Hook.purs
Outdated
useEffect | ||
:: forall a | ||
. Effect (Effect a) | ||
-> Maybe (Array EffectInput) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to not go ahead and lose the Maybe
. There are two "empty" cases with Just []
. If a user always has to pass in something they might as well pass in []
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that React behaves differently if you pass an empty array vs. not passing an array at all.
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect
Not passing an array means that the effect is fired after every render. Passing an empty array means that the effect is fired only on mount and unmount.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/React/Hook.purs
Outdated
callbackInput :: forall a. a -> CallbackInput | ||
callbackInput = unsafeCoerce | ||
|
||
foreign import data CallbackInput :: Type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is the same as EffectInput
, maybe there should be a shared MemoValue
or something that they both use instead. The only reason this exists is to erase the type, so even Foreign
would be appropriate, but I don't think it's necessary to have new types for each one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started with a shared type for these. I would be up for making one type. Probably cleaner that way. Good call.
. (Unit -> a -> b) | ||
-> Maybe (Array MemoInput) | ||
-> Hook (a -> b) | ||
useMemo k = runFn2 useMemo_ k <<< Nullable.toNullable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think useMemo
necessarily computes a function. It is supposed to memoize an arbitrary value (useCallback
is just a shortcut for memoizing a function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. I will review this. I haven't played with it too much yet. Thanks.
. Ref a | ||
-> (Unit -> { | r }) | ||
-> Maybe (Array ImperativeMethodsInput) | ||
-> Hook Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this type. useImperativeMethods
depends on forwardRef
, which will pass in a second argument to your component. We don't expose this anywhere, so I don't know if Ref a
is the correct type. I think it would actually be very hard to make use of this in even a remotely type-safe way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that this may not be the best representation. I still need to work this one out a bit more. I am open to suggestions on this.
(Hook Unit) | ||
|
||
useContext :: forall a. Context a -> Hook a | ||
useContext = runFn1 useContext_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, this isn't usable since Context
is a foreign type here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In hindsight, I should have made Context
a foreign type to begin with instead of wrapping it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it is worth making Context
a foreign type? It's a breaking change, but maybe it is an idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with the breaking change. It's very minor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I've changed Context
over to a foreign type.
Also, in order to render the consumer, I found I needed a createRenderPropsElement function. Was there another way to render the consumer?
On a side note, it seems refs have also been updated in React 16.3. I've incorporated those changes as well since it fits in with the hooks API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just used createLeafElement
, and supplied the children
prop explicitly in those cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, okay. Having createRenderPropsElement
might be useful. What do you think? I am not sure about the name though.
:: forall props | ||
. ({ | props } -> Hook ReactElement) | ||
-> { | props } | ||
-> ReactElement |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
createHookElement
like createLeafElement
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this all uses createElement
under the hood, this isn't parametric due to key
and ref
props.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good for createHookElement
. Noted on key
and ref
.
@natefaubion Thanks for taking a look at all of this! |
Example usage of hooks: https://github.com/ethul/purescript-react-example/tree/hooks |
@@ -332,7 +331,7 @@ class ReactPropFields (required :: # Type) (given :: # Type) | |||
|
|||
type ReservedReactPropFields r = | |||
( key :: String | |||
, ref :: SyntheticEventHandler (Nullable ReactRef) | |||
, ref :: Ref DOMRef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct. A class component isn't going to have a DOMRef
since it will be pointing to the instance. As is, the only thing that would make sense to me is Foreign
. Since we are redoing refs anyway, maybe there's a better way to handle this side of the interface in a typed way... I'm not sure though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. It could be the instance. I wouldn't mind Foreign
, but what if we made an opaque type ForwardRef
instead? I am not sure if there is a better way to hand this, but I am open to ideas.
It's definitely worth looking at the |
Thanks for pointing out the On another note, and as a point for discussion, I suppose I am a bit on the fence about tracking hooks via the index-monad, which seems along the lines of how |
Should this PR be closed? AFAIK, most people use |
Proposal for supporting React Hooks.
This code is still in the early stages and requires some ideas to be worked out further, but perhaps it is a useful start for a discussion. Any comments, questions, or suggestions are most welcome.
// @natefaubion