Skip to content

RFC: Privatize some options (-7 B)#1692

Merged
marvinhagemeister merged 5 commits into
masterfrom
privatize-options
Jun 10, 2019
Merged

RFC: Privatize some options (-7 B)#1692
marvinhagemeister merged 5 commits into
masterfrom
privatize-options

Conversation

@andrewiggins

@andrewiggins andrewiggins commented Jun 8, 2019

Copy link
Copy Markdown
Member

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.ts I'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.

@coveralls

coveralls commented Jun 8, 2019

Copy link
Copy Markdown

Coverage Status

Coverage remained the same at 99.778% when pulling c2554e1 on privatize-options into 93c626e on master.

@andrewiggins andrewiggins left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread src/index.d.ts
*/
interface Options {
/** Attach a hook that is invoked before render, mainly to check the arguments. */
root?(vnode: ComponentChild, parent: Element | Document | ShadowRoot | DocumentFragment): void;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, it's mainly needed for devtools and I doubt there is much use beyond that 👍

Comment thread src/index.d.ts
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread src/index.d.ts
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! It will likely change a lot if we'll work on 2-phase commit 👍

Comment thread src/index.d.ts
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 👍

Comment thread src/index.d.ts
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/index.d.ts Outdated
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _ancestorComponent pointer (soon to be _parent) is a private field and not accessible to users, which I think reduces the value of a catch* 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 On making it private

Comment thread src/index.d.ts Outdated
*
* @return Return a boolean indicating whether the error was handled by the hook or not
*/
catchRender?(error: any, component: Component): boolean;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment for catchError

Comment thread src/index.d.ts Outdated
/** 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;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/index.d.ts
*/
catchRender?(error: any, component: Component): boolean;
event?(e: Event): void;
requestAnimationFrame?: typeof requestAnimationFrame;

@andrewiggins andrewiggins Jun 8, 2019

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@marvinhagemeister marvinhagemeister left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🥇

@robertknight

Copy link
Copy Markdown
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants