-
Notifications
You must be signed in to change notification settings - Fork 9
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
Replace old ReasonReact-like API with Hooks #27
Conversation
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.
This looks amazing @wokalski !! It's really great to see how hooks simplify many parts of the code. I was also surprised by how the implementation of hooks in Hooks.re
takes just a few lines 😮
I had just a few questions, that maybe others reviewing the PR could also share.
@jchavarri do you have any ideas how to implement a PPX rewriter for this hook API? |
@wokalski I had a wip branch called Note this ppx was highly experimental, and it was applied to the continuation-passing style API we explored at some point. But it was working for that case (at least the happy path), and it might help as starting point for the slots ppx. Relevant files: One thing I was also planning to add are some tests for it. I was taking a look at a section called "Testing the rewriter" in http://rgrinberg.com/posts/extension-points-3-years-later/#testing-the-rewriter. |
@jchavarri i see. My main conceptual problem with the ppx is how do we pass the initial slots value |
@wokalski One would need two ppxs: one for the component and one for the hook. For example: let%comp counter = (_children) => {
let%hook (counter, setState) = Hooks.useState(0);
<Button onClick={() => setState(counter + 1)} />;
}; or with a module ppx, which is maybe more "legit" about what is happening behind the scenes, but still can alleviate all the work of manually calling module%comp Counter = {
let render = _children => {
let%hook (counter, setState) = Hooks.useState(0);
<Button onClick={() => setState(counter + 1)} />;
}
}; both would be transformed into: module Counter = {
let component = component("Counter");
let make = _children =>
component(_slots => {
let (counter, setState, _slots) = Hooks.useState(0, _slots);
<Button onClick={() => setState(counter + 1)} />;
});
let createElement = (~children, ()) => element(make(children));
}; The |
@jchavarri great! I think we're on the same page, I've also been thinking about such PPX! I'm not sure what should be the "custom" hook ppx. I.e. how to name it. The bonus point of a ppx is that we can use an invalid ocaml identifier as the variable name - it's very unlikely for it to shadow anything in such case! |
I guess you mean "custom" component ppx? Maybe we could use
For the slots identifier, right? That's a great point! Using an invalid identifier removes all chances of collisions 💯 |
@jchavarri I meant the custom hook, ie custom hook function. I realise it's "just a function" but if we use a special identifier for When it comes to the component ppx I guess we have to see. I'm more inclined towards lower-case components because they are less verbose and maybe we could leave the upper case PPX for something else! |
What do you mean? Wouldn't the author of the custom hook decide whatever name for |
@jchavarri if |
I agree. 👍 Maybe I was confused because I wasn't thinking custom hooks authors would use the i.e. I was thinking something like this: /* Some custom hook, doesn't use any ppx */
let useCustomHook = (prop, slots) => { let foo = useState(prop, slots); ... }
let%component c = (~prop, ()) => { let%hook x = useCustomHook(prop) } |
98c3183
to
12779bd
Compare
The data structure used for accumulating enqueued effects is extremely inefficient and there are almost no tests for the effect hook. That said I would like to address those issues in a separate PR. I would love to, however, get some feedback on the implementation of hooks themselves and what you think about the API. I think the most relevant modules are: |
core/test/Assert.re
Outdated
|
||
let executeSideEffects = ({renderedElement} as testState) => { | ||
RenderedElement.executeHostViewUpdates(renderedElement) |> ignore; | ||
List.iter(f => f(), renderedElement.enqueuedEffects); |
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.
It looks like enqueuedEffects
isn't available on the renderedElement
via ReactCore
.
Is it expected that these effects would be ran as part of executeHostViewUpdates
? Or will there be a separate API, like executeEffects
?
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.
There has to be a separate API. The reason is that we normally want to call executeHostViewUpdates
on the main thread but the effects should be executed on a background one.
Makes sense. The 'Effect' API looks good to me, aside from one comment I left - I'm wondering if there will be a separate API for executing the effects, or if its expected to occur as part of Aside from that - seems like the API surface will be the same, and its just about fixing bugs / streamlining the syntax with PPX / etc. 👍 Thanks for all your hard work on this @wokalski ! |
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.
This looks awesome @wokalski !! 👏 👏 👏
@@ -0,0 +1,266 @@ | |||
type hook('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.
Just curious, what's the advantage of the "open variant" approach? as opposed to define hook('a)
after the modules?
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 know... yet. I thought it might be an interesting idea to implement hooks as an open variant to allow users creating custom types of hooks in the userspace. Like what if we exposed enough internals (but not too much!) to implement Hooks.suspense
in the user space? I think it could be very interesting.
stateContainer := {currentValue, updates: [nextUpdate, ...updates]}; | ||
}; | ||
|
||
let hook = (~initialState, reducer, hooks: hooks(_)) => { |
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.
nit: initialState
is not a named arg in Ref
and 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.
That's on purpose. Ref
and State
are single argument + hook so it's pretty self explanatory and for the reducer you know you have to put the reducer in but you might forget the order for instance so we add this explicit ~initialState
argument.
onSlotsDidChange, | ||
}; | ||
|
||
module 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.
As a fun idea, in reason-reactify
we used to implement useState
as a special case of useReducer
which allowed to reduce some of the boilerplate. Not sure if it'd be interesting in this case :)
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.
they were implemented differently on purpose. My thought process is that the operation in the reducer might take relatively long to compute and actions are very often fired during interactions. Enqueuing an update takes constant time. Also, with this approach it'll be relatively easy to implement UpdateWithSideEffects
. That said I'm not sure about this myself 😕
core/lib/Hooks.re
Outdated
let enqueueUpdate = (nextUpdate, stateContainer) => { | ||
let {currentValue, updates} = stateContainer^; | ||
|
||
stateContainer := {currentValue, updates: [nextUpdate, ...updates]}; |
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.
Why does Reducer
follow a different strategy than State
? Would it be possible to instead of a list of updates, call the reducer and store the value in nextValue
as the state hook does?
| Update; | ||
type always; | ||
type onMount; | ||
type condition('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.
Trying to wrap my head around this... if this is already a GADT, does the 'a
need to be exposed? Would something like this be possible?
type condition =
| Always: condition
| OnMount: condition
| If(('a, 'a) => bool, 'a): condition;
Or alternatively, make it a regular variant / ADT.
I guess I'm not understanding why this is a GADT 😄
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.
Good question. The reason is that I want the constructor to carry more type information so that you cannot do this:
Hooks.effect(someProp ? Always : OnMount, () => None)
Always
and OnMount
have different types so the semantics here would be very confusing!
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.
also, for the if condition we have to know what type the compared value is!
core/lib/Slots.re
Outdated
|
||
module type S = { | ||
type elem('a); | ||
type opaqueElement = |
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 got slightly confused about element
. It's not the reconciler "element", right? This would be more the "content" or the "stored data type" the slots will contain? 🤔
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.
great suggestion!
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.
Fixed!
cc24a0a
This commit should also the reduce of allocations.
cc24a0a
to
c80f053
Compare
d755625
to
e31c40f
Compare
This PR introduces basic support for a React-Hook like API. The only supported hooks are
useState
anduseReducer
. ImplementinguseEffect
will probably require some architectural overhaul to the current naive Slots approach. Also, there's a lotof mutation everywhere now. It'd be great to minimise this in the next version.
Temporary component API
If you look at the tests the component looks like this:
This API is temporary because it doesn't require a PPX for JSX. The planned API is: