My name is PorfΓrio and I work at Agroop for 3 years, building an App using React.
I'm always curious with new technologies and ways to do things, so I started testing React Hooks since the first day it was announced and started using them in production right after the first release.
So when I heard all the fuzz around an RFC in Vuejs because of a new API I started looking at it and try to understand what it was.
After the initial fuzz, they get to set a quite interesting API
At the time I was also reading Preact source, the advantage of having a small library, is that it is possible to read the code and understand most of it.
Infact Preact source for Hooks, had helped me understand how Hooks work, in a way I was not able to do when I tried to read React source. And I found out this interesting API in Preact that let you hook into the rendering process and thats what they use to introduce Hooks into Preact as a separate bundle without increasing Preact size
Has I enjoyed so much the new Vue API and I was messing with Preact I decided to create a proof of concept of implementing the new Vue Composition API on Preact.
You can find it here: https://github.com/porfirioribeiro/preact/blob/composition-api/composition/src/index.js
Meanwhile I created a WIP PR on preact repo: https://github.com/preactjs/preact/pull/1923
Of course that there are diferences from the Vue api, as both libraries handle things differently.
Comparing with Vue Composition API
https://vue-composition-api-rfc.netlify.com
Preact Composition API is heavily inspired by Vue, so it's API tries to mimic Vue API but it's not exactly the same, due to some limitations or by design.
createComponent / setup
Vue uses createComponent
accepts an object that includes setup
, that is Vue way to define components, with objects. In fact,createComponent
does nothing, mostly helps with typing.
In Preact createComponent
accepts a function that returns a function component. It does not do much in Preact either, it just marks that function as a Composition function so Preact can handle it diferently.
export const MyComp = createComponent(() => {
// composition code
//
// return function component
return ({ message }) => <div>{message}</div>;
});
reactive
reactive
wraps an object in a proxy so that every time the object is changed the component is updated, working as a state holder.
export const MyComp = createComponent(() => {
const obj = reactive({ count: 0 });
function increase() {
obj.count++;
}
return ({ message }) => (
<div onClick={increase}>
{message} {obj.count}
</div>
);
});
ref
ref
is also a state holder, mostly it wraps one value, we need this as in JavaScript natives are passed by value, not reference.
When theRef.value
is changed, the component is updated.
The implementation of ref
is more simple than reactive
as it uses an object with getters/setters.
export const MyComp = createComponent(() => {
const count = ref(0);
function increase() {
count.value++;
}
return ({ message }) => (
<div onClick={increase}>
{message} {count.value}
</div>
);
});
isRef
isRef
returns if a object is a ref
unwrapRef
try to unwrap the ref
const value = isRef(foo) ? foo.value : foo; //same as
const value = unwrapRef(foo);
toRefs
toRefs
is not implemented yet as the design of the API in Preact is different of the Vue one, didn't found a good use for it, yet.
computed
computed
is not implemented as is, it's mostly joined with watch
as the Preact life cycle works a little different from Vue
watch
watch
in Preact is a bit different from watch
in Vue, due to the differences from Preact and Vue, and also some API design to support other Preact features like Context
Because of that nature, we have 2 functions alike: watch
and effect
watch
runs before render and can return a ref
with the result of it's execution
effect
is run after update, as a side effect
//un-exhausted example of what watch can do!
const countGetter = props => props.countProp;
export const MyComp = createComponent(() => {
const countRef = ref(0);
const reactiveObj = reactive({ count: 0 });
const memoizedComputedValue = watch(
[countRef, reactiveObj, countGetter],
// this will be computed when any of those 3 dependencies are updated
// works as computing and memoization
([count, obj, countFromProps]) => count * obj * countFromProps
);
effect(
memoizedComputedValue,
value => (document.title = `computed [${value}]`)
);
function increase() {
countRef.value++;
}
return ({ message }) => (
<div onClick={increase}>
{message} {memoizedComputedValue.value}
</div>
);
});
lifecycle-hooks
Only some lifecycle hooks are implemented, some not implemented yet, others will not be implemented as it does not make sense or can't be implemented in Preact
-
onMounted
Callback to call after the component mounts on DOM -
onUnmounted
Callback to call right before the component is removed from DOM -
effect
cannot be considered a lifecycle, but can be used to achieve the same asonUpdated
in Vue, tracking the needed dependencies.
provide-inject
provide
and inject
is not implemented as Preact already have a Context API, probably it can be implemented later.
We can achieve inject
like feature by passing a Context as src on watch
or effect
, making the component subscribe to the closest Provider of that Context
export const MyComp = createComponent(() => {
const userCtx = watch(UserContext);
return ({ message }) => (
<div>
{message} {userCtx.value.name}
</div>
);
});
Comparing with (P)React Hooks
https://reactjs.org/docs/hooks-reference.html
At the first look we might find React hooks and Preact Composition API(PCApi) alike, but there is a HUGE difference between them.
The function passed to createComponent
when we call the composition functions is only executed once during the component lifecycle, the returned function component is executed at each update.
And in React the hooks are always called and (most of it) redefined in each render, Vue has a good explanation of the differences
This has to bring a mind shift, in hooks you can deal with simple variables but have to deal with code re-declaration and memoizing of values and callbacks to avoid children re-renders.
useState
useState
is used in React as a state holder, in PCApi ref
or reactive
can be used, depending on the need of holding a single value or multiple value object
// (P)React hooks
const Counter = ({ initialCount }) => {
// redeclared and rerun on each render
const [count, setCount] = useState(initialCount);
const reset = () => setCount(initialCount);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
return (
<>
Count: {count}
<button onClick={reset}>Reset to {initialCount}</button>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};
// Preact Composition
const Counter = createComponent(props => {
// run once
const countRef = ref(props.initialCount);
const reset = () => (countRef.value = props.initialCount);
const increment = () => (countRef.value += 1);
const decrement = () => (countRef.value -= 1);
return ({ initialCount }) => (// run on each render
<>
Count: {countRef.value}
<button onClick={reset}>Reset to {initialCount}</button>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
});
Both implementations have mostly the same size and code looks alike, the difference is mostly that the composition functions only run once and the callbacks are not redeclared in each render.
It might not matters much, but having to swap event handlers in each render is not optimal and one of the reasons why React implemented SyntheticEvents.
useEffect
useEffect
is a all in one effect handler, you can use it for mount (onMounted
)/unmount(onUnmounted
) lifecycles or for update based on dependencies.
// (P)React
const Comp = props => {
useEffect(() => {
// subscribe
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
}, []);
return <div>irrelevant</div>;
};
// Preact Composition
const Comp = createComponent(props => {
let subscription;
onMounted(() => {
// subscribe
subscription = props.source.subscribe();
});
onUnmounted(() => {
// Clean up the subscription
subscription.unsubscribe();
});
return () => <div>irrelevant</div>;
});
Again, code for both approaches are mostly alike, useEffect
will check dependencies and find the empty array making the effect never change and bail out the new function.
Now if you need to subscribe based on some dependency (eg. a prop) its a little bit difference.
// (P)React
const Comp = props => {
useEffect(() => {
const subscription = props.source.subscribe(props.id);
return () => subscription.unsubscribe();
}, [props.id, props.source]);
return <div>irrelevant</div>;
};
// Preact Composition
const Comp = createComponent(props => {
effect(
props => [props.id, props.source],
([id, source], _oldArgs, onCleanup) => {
const subscription = source.subscribe(id);
onCleanup(() => subscription.unsubscribe());
}
);
return () => <div>irrelevant</div>;
});
effect
gives you 3 things, newArgs, oldArgs (in case of update), onCleanup that is a special function that you can call and pass a cleanup function. It does not use the return callback aproach because effect callback may be async!
useContext
useContext
let you subscribe and get the value of a context in a parent component, in Composition API you can use the context as a source of a watch or effect function.
// (P)React
const Comp = props => {
const ctxValue = useContext(MyContext);
return <div>{ctxValue}</div>;
};
// Preact Composition
const Comp = createComponent(props => {
const ctx = watch(MyContext);
return () => <div>{ctx.value}</div>;
});
watch
gives you some advantages and let you connect many sources together!
useReducer
There is no useReducer
alternative yet, but it could be easly implemented
useCallback
In most scenarios, a useCallback
like function is not necessary, as you can define your callbacks at setup time only once and the reference will never change, thats one of the great features of this API.
Normaly your callacks are called sync, so you can access your state and props references with the right values, but sometimes you may be passing a function to a component that will be called at a different time and you want that to be called with the current value.
// (P)React
const Comp = props => {
const handlePostSubmit = useCallback(
() => console.log('This will be called with actual id', props.id),
[props.id]
);
return <Form onPostSubmit={handlePostSubmit}>irrelevant</Form>;
};
// Preact Composition
const Comp = createComponent(props => {
const handlePostSubmit = watch(
props => props.id,
id => console.log('This will be called with actual id', id)
);
return () => <Form onPostSubmit={handlePostSubmit.value}>irrelevant</Form>;
});
useMemo
useMemo
allows you to memoize values and avoid recalculating big values unless its need
// (P)React
const Comp = props => {
const [filter, setFilter] = useState('ALL');
const filteredItems = useMemo(() => filterItems(props.items, filter), [
props.items,
filter
]);
return <ItemList items={filteredItems} />;
};
// Preact Composition
const Comp = createComponent(() => {
const filterRef = ref('ALL');
const filteredItems = watch(
[props => props.items, filterRef],
([items, filter]) => filterItems(items, filter)
);
return () => <ItemList items={filteredItems.value} />;
});
useRef
useRef
is used mainly for 2 things, handle DOM references and save components values between renders
As we have the setup function, all var's declared there can be used between renders, so no useRef
needed.
For DOM values you can use callbacks and local var's or React.createRef
useImperativeHandle
Haven't found a need for it yet, but I beleve it can be implemented
useLayoutEffect
At the moment there is no direct replacement for this.
useDebugValue
Haven't found a need for it yet, but I beleve it can be implemented
Conclusion
The point here is not to say that this API is better, its different, both have pitfals as Evan You as pointed on Twitter: https://twitter.com/youyuxi/status/1169325119984082945
Top comments (5)
I like it! Better than hooks. π
I only wish you'd do the obvious, idiomatic thing - instead of hiding the current component in a global variable, make it plain what's actually going on:
Yes, slightly more repetitive - but takes all the initial mystery out of it, and avoids teaching newbs how to be "clever".
I don't know why anybody thinks it's more "elegant" to hide things in global state.
In my world, obvious beats clever, every time. Code should do what it looks like it does. That's my only real gripe with hooks. π€·ββοΈ
I like that concept also, it can solve some issues, but may introduce others.
Using named exports alow for better tree-shaking and better minification, making all composition functions optional.
The way you specify it needs to create a object with all the functions binded.
Thanks for bringing this to discussion. We can find the pros and cons of both approachs and decide where to go!
True.
But it doesn't have to be OOP - that was just an example.
We can do the same thing with functions:
My only point is don't hide your dependencies in global state.
I see your ideas.
Would be nice to discuss this better. Could you post your concern on the PR github.com/preactjs/preact/pull/1923
or on Preact Slack
preact.slack.com/
Done π