RFC: Privatize some options (-7 B)#1692
Conversation
e35ca76 to
1645fba
Compare
andrewiggins
left a comment
There was a problem hiding this comment.
I believe the options that remain serve as a reasonable back-compat story for the options from Preact 8. Let me know what you think!
| */ | ||
| interface Options { | ||
| /** Attach a hook that is invoked before render, mainly to check the arguments. */ | ||
| root?(vnode: ComponentChild, parent: Element | Document | ShadowRoot | DocumentFragment): void; |
There was a problem hiding this comment.
I think in user-land, most things you can do with root could be accomplished by just wrapping render. I think this pattern of composing functions (e.g. let render = wrapper(render)) is a much more powerful pattern that can do what this hook does and more, and so I think I'd encourage our users to do that over use this root hook.
There was a problem hiding this comment.
Agree, it's mainly needed for devtools and I doubt there is much use beyond that 👍
| /** Attach a hook that is invoked before render, mainly to check the arguments. */ | ||
| root?(vnode: ComponentChild, parent: Element | Document | ShadowRoot | DocumentFragment): void; | ||
| /** Attach a hook that is invoked whenever a VNode is created. */ | ||
| vnode(vnode: VNode): void; |
There was a problem hiding this comment.
Note: I just fixed the vnode type to specify that this is optional (i.e. vnode?()). I'm not suggesting we remove this hook. It's arguably our most powerful!
| /** Attach a hook that is invoked whenever a VNode is created. */ | ||
| vnode(vnode: VNode): void; | ||
| /** Attach a hook that is invoked after a tree was mounted or was updated. */ | ||
| commit?(vnode: VNode): void; |
There was a problem hiding this comment.
I think our idea of a commit phase is pretty new and not fully fleshed out yet. I think I'd like to hold back on what a commit hook looks like until we have explored (or decided not to explore) a fully two-phased render. Depending on how we do that, this hook may look very different!
There was a problem hiding this comment.
Good call! It will likely change a lot if we'll work on 2-phase commit 👍
| /** Attach a hook that is invoked before a vnode is diffed. */ | ||
| diff?(vnode: VNode): void; | ||
| /** Attach a hook that is invoked before a vnode has rendered. */ | ||
| render?(vnode: VNode): void; |
There was a problem hiding this comment.
Could this be accomplished with a vnode hook that wraps a component's render method, similar to the wrapping I referred to in root? Wrapping the render method would probably be way more useful cuz that way the wrapper gets access to the component (through this) and the vnode (as the parameter to the vnode hook). Currently a public render option wouldn't be able to access the component of the VNode cuz it's a private property on the VNode.
There was a problem hiding this comment.
I like this idea, never thought about it that way 👍 That would be a lot more powerful and would likely be easier to understand for users 👍
| /** Attach a hook that is invoked before a vnode has rendered. */ | ||
| render?(vnode: VNode): void; | ||
| /** Attach a hook that is invoked before a hook's state is queried. */ | ||
| hook?(component: Component): void; |
There was a problem hiding this comment.
Hooks are pretty new and there might still be some bytes we can squeeze out of them yet! This is one option that we could perhaps leave in if someone feels strongly, but absent of that, I still think I'd prefer to not expose it by default.
There was a problem hiding this comment.
Yup, we can remove it. A hook like this or something else will be needed for the devtools in the future. It needs some way to know which hook was called. But again this can be postponed and I agree with your general sentiment that we should only expose them if they are really needed.
| /** Attach a hook that is invoked after a vnode has rendered. */ | ||
| diffed?(vnode: VNode): void; | ||
| /** Attach a hook that is invoked after an error is caught in a component but before calling lifecycle hooks */ | ||
| catchError?(error: any, component: Component): void; |
There was a problem hiding this comment.
I have a couple issues with the catchError and catchRender options:
- These two options expose that we have multiple try/catch's in our codebase. Will that always be true? I'd personally like to reduce that number to save bytes but having to support both these options may make that difficult/less useful.
- The component passed in to the option is the first parent of the throwing component. Is that the right thing to pass in here? Or would user's want/expect to be given the component that threw? Without any good user scenarios, it's hard to know what to do here.
- The
_ancestorComponentpointer (soon to be_parent) is a private field and not accessible to users, which I think reduces the value of acatch*hook. Since we currently pass in the parent component (which may or may not be an error boundary), I'm uncertain what a good use case for this option is.
There was a problem hiding this comment.
+1 On making it private
| * | ||
| * @return Return a boolean indicating whether the error was handled by the hook or not | ||
| */ | ||
| catchRender?(error: any, component: Component): boolean; |
There was a problem hiding this comment.
See comment for catchError
| /** Attach a hook that is invoked immediately before a vnode is unmounted. */ | ||
| unmount?(vnode: VNode): void; | ||
| /** Attach a hook that is invoked before a vnode is diffed. */ | ||
| diff?(vnode: VNode): void; |
There was a problem hiding this comment.
I'm a little torn on this one. I'm not sure how it could be used, but it feels complimentary to diffed. Perhaps we should leave it out until we have a reason not to? Not sure...
There was a problem hiding this comment.
Yeah, this hook is tough. Wondering if most of it could be accomplished by mutating a vnode in options.vnode() instead. This one is mainly needed for profiling purposes as we need to have a marker when the node actually starts and finishes rendering.
There was a problem hiding this comment.
I went ahead and marked it as private. If we want to bring it back it's as simple as reverting the latest commit, so just let me know.
| */ | ||
| catchRender?(error: any, component: Component): boolean; | ||
| event?(e: Event): void; | ||
| requestAnimationFrame?: typeof requestAnimationFrame; |
There was a problem hiding this comment.
I figured it made sense to leave this one as public since it is a core scheduling primitive of useEffect hooks, similar to how debounceRendering is the scheduling primitive for state updates
1645fba to
1d8d60f
Compare
marvinhagemeister
left a comment
There was a problem hiding this comment.
This is a very good direction we're moving in with this PR 👍 Only exposing options that are actually needed makes it a lot safer to explore with various implementations of our internals 💯 Outstanding work as usual Andre 🥇
|
I'm in agreement with the broad principles outlined in the PR description. While Preact 10's internals are still new and evolving, I think it makes sense to be conservative with the options hooks that are exported. Also, if someone does have a need for a private hook, that will hopefully lead them to file an issue and give us useful information about their use case. |
Our options hooks are an important and useful part of our library that serve many purposes. For examples, see Marvin's blog or #1690.
However, given our current bundle size (~3.4k) many changes going forward will likely need to remove or replace code in order to remain small and stay within our size budget (e.g. #1688). In other words, PRs may need to refactor internals in order to stay small. Exposing too many internal details of how our diff works makes these kind of refactors more difficult and could prevent us from making the right changes to stay within our size budget.
My concern is that our current set of options may expose publicly too many internal details and could impact our ability to keep our size down in the future. While working on #1688 I had to make a breaking change to our options. While it's possible that the specific issue in #1688 could be resolved differently, it made me worried that for other changes, we wouldn't be able to adjust our internals to reduce the impact of the PR. Of course, while all libraries need to abide by the public API they ship and not break it unnecessarily, I'd like to reduce our current public API surface to enable more kinds of changes than the current API allows - specifically privatize some of our options.
Again, I really think our options are an important part of public API, and for all the options I suggest removing, I think we can bring them back in the future. But I think I'd like to release Preact X with a minimal set, and only make more options public once some new features have stabilized more (e.g. Suspense) and we have compelling user scenario(s) an option lights up.
Note: this isn't a comment on internal usage of options. We can add/remove as we please since they are all internal and served from the same package.
Thoughts?
Cheers,
Andre
P.S. Below in
index.d.tsI've left comments under each option I'm proposing to privatize. If you have thoughts about a specific option, leave it in the thread there.