Skip to content

Instantly share code, notes, and snippets.

@porfirioribeiro
Created January 6, 2020 23:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save porfirioribeiro/a73599b881b155bda52380ca04e433be to your computer and use it in GitHub Desktop.
Save porfirioribeiro/a73599b881b155bda52380ca04e433be to your computer and use it in GitHub Desktop.
Preact Composition API

Preact Composition API

  • Toughts and doubts
  • Reactivity
  • Current implementation
  • Component based implementation

Toughts and doubts

  • displayName

  • defaultProps

  • memo

  • Ref's

displayName

Components created with composition will not have a displayName to use on dev tools, making it harder to debug.

I guess a babel plugin could be done to make it add a MyComponent.displayName='MyComponent'; automaticly

defaultProps

To make defaultProps to work on composition components we have to use plain old .defaultProps like we use in class Component's:

const MyComponent = createComponent(c => {
	console.log(c.props);
	return props => <div>{props.hello}</div>;
});

MyComponent.defaultProps = {
	hello: 'Hey there'
};

Probably we could also accept optionally an object on createComponentwith some options like:

const MyComponent = createComponent({
	defaultProps: {
		hello: 'Hey there'
	},
	displayName: 'Mycomponent',
	setup: c => {
		console.log(c.props);
		return props => <div>{props.hello}</div>;
	}
});
memo

There is an implementation of memothat can make a composition component skip renders if nothing changes. By default it checks props only using shallowDiffers, we can pass a function to specify what you really want to check, kinda like how memo in React works. For that reason, probably it should be named differently to not make confusion with memofrom preact/compat maybe?

const MyComponent = createComponent(c => {
	memo();
	return props => <div>{props.hello}</div>;
});

Or if we accept the options object on createComponent maybe we can pass memo: true or a function to custom check props?

const MyComponent = createComponent({
	memo: true,
	setup: c => {
		return props => <div>{props.hello}</div>;
	}
});

Either way, memowill only check props, there is no way currently to skip render on state change. Actually exactly how memo in React works.

ref

ref's in React always seems like a second class citizen, in fact we should avoid it when we can, but when you are defining a component library, you never know if the other developer will need to use ref's on your FancyButtonso we end up doing forwardRef on every component. Because of that i decided to implement the components in Composition API to always pass refas a second parameter on the render function.

The problem is if that ref is not assigned to any child, and you pass a ref to this component, that ref will be lost in space and will never be assigned on an error be trowed. So something must be done to make this more clear. Either some way to know that the ref if going to be needed and if not fallback to the default (pass the instance of the Component):

const MyComponent = createComponent(c => {
	const ref = refForwarded();
	return (props, ref) => <div ref={ref}>{props.hello}</div>;
});

Or add an option to it like:

const MyComponent = createComponent({
	ref: true,
	setup: c => {
		return (props, ref) => <div ref={ref}>{props.hello}</div>;
	}
});

Apart from that, there is a something i dislike of the current implementation of the ref forwarding system.

Ref is removed from props on vnode

https://github.com/preactjs/preact/blob/master/src/create-element.js#L15

Then added back again on composition vnode hook:

https://github.com/preactjs/preact/pull/1923/files#diff-23369f01c5ea98c3654a47ca1abd2de7R7-R17

And extracted again before calling render:

https://github.com/preactjs/preact/pull/1923/files#diff-23369f01c5ea98c3654a47ca1abd2de7R33-R39

This looks a bit hackish to me and i wish to find a way to simplify this in a way that makes this more direct. Maybe a tiny concept of forward ref on core like this:

https://github.com/preactjs/preact/pull/2156/files#diff-9722053b2275277f66f76c1671e478bfR18

Or have some way deal with ref's, like some hook or something...

My first implementation didn't add ref back on the props, used a different variable, but then there was no way to check in on shouldComponentUpdate and i needed to check if ref changed on memo, maybe if shouldComponentUpdate could know what is the nextVNode to be able to compare the ref with the oldVNode, that i can acces in this._vnode

Reactivity

Now either effect or watch will have it's sources evaluated before each render to see if they need to update/execute the callback.

That needs to be done because one of its sources may be a function that return a diferent value based on props. So if the component rerender because the prop change, the watch/effect will be executed, or not if it didn't.

But watch/effect may not have props watcher, it may be watching some value or reactiveor even the return of another watch! So i tought about creating a more observable approach. The return of value, reactive and watch will return an object that can be observable so any watch/effect using it as a source will only be executed when they actually change and not on every render. It also need to keep track of every watcher (it already does so...) and on unmount unobserve all sources being watched to avoid memory leaks.

But that keeps 2 things out of this approach: functions that return prop and context (from createContext)

To fix this, we need a way to make them reactive, it can be done keeping the same API and do that behind, or separate it and create a specific API for this:

const SomeContext = createContext('context');
const MyComponent = createComponent(c => {
	const someReactiveProp = watchProp(() => c.props.hello);
	const someReactiveContext = watchContext(SomeContext);
	effect(
		[someReactiveProp, someReactiveContext],
		([prop, ctx]) => prop + ' ' + ctx
	);
	return props => <div>{props.hello}</div>;
});

Maybe at some point we can also add watchObjervable to make this work with RxJS observables.

At the moment functions passed to watch/effect receive the current props, thats nice but makes it impossible to play nice with TypeScript, so i think it might be simpler to get the props from c arg passed to createComponent function, that can be easly typed

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