Open danielkcz opened 5 years ago
@zouloux Of course, you are right that maybe I am "too much int the React syntax". But there are wo things that IMHO everybody should be aware of:
There is a difference between app state and local component state: app can be really complex and will increase while the app is growing. Local component state on the other hand should always be limited to as less complexity as possible. If you have a very complex local component state then maybe you should split your component in smaller pieces. Also app state may by a very deep data structure while you local component state hopefully will be much less hierarchical. Handling local component state should be much much easier than handling app state, so do you really need some fancy "reactive" system with automatic change detection inside of your component just for that arguable easily manageable local component state?
And the most important thing is: When it comes to handling component state, there is really THE!!! ONE obvious solution: State transition will be defined by pure functions that get the immutable old state plus maybe some other arguments and return the immutable new state. It doesn't really matter whether you use a reducer combined with a dispatch
function or a setState
function, the transition itself will always be defined by a pure function (of course the trigger functions dispatch
and setState
themselves are impure).
And pure functions are the by far best thing in programming. While on the other hand, of all "normal" things in programming, effectful functions are one of the worst.
I'm really willing to got the "reactive" way to handle local component state, but the only thing I ask for before I go that step is that somebody gives me one very, very good reason why this is better than using pure reducer functions - and nobody has done that yet....
@zouloux Regarding Svelte: There is no doubt that if someone provides a way to write components and apps in less lines of code and the output bundles are also much smaller and the app may be much faster then for sure this person deserves really all my respect (and btw the author is undoubtably a really smart guy ... and IMHO has a really great sense of humor - I really love that guy).
But this "reactive declarations" feature (https://svelte.dev/examples#reactive-declarations) which uses syntactical valid JavaScript code but changes its semantics entirely is a complete deal breaker for me, and frankly, statements like the following are not really convicing me:
Don't worry if this looks a little alien. It's (if unconventional) JavaScript, which Svelte interprets to mean 're-run this code whenever any of the referenced values change'. Once you get used to it, there's no going back.
@mindplay-dk Thanks for the demo. I have some question regarding this:
1.) Let's say you want the counter to have a default value of 100 and a default title of 'Counter'.
I personally prefer declaring my defaults inline, where they're needed - line 215 shows how to initialize with a default value of 0
:
const [state, setState] = counter.useState(({ value }) => ({
count: value || 0
}));
2.) In line 224: Let's say you also want to print out the title of the counter. Would you do the following or what else would you do?
console.log(`You clicked "${counter.props.title}" ${state.count} times`)
Yep, counter
is the Component
instance, which has the current props.
That's not super elegant, but this is just an experiment / proof-of-concept.
3.) Let's say you want to write a hook function
useWindowSize
what TypeScript type would that function have? Same question for a hook functions that returns a bundle of 10 different things.
Custom hooks just need to accept the component instance as an argument, so:
function useWindowSize(component) {
component.useState(...);
component.useEffect(...);
// etc.
}
And then apply it with:
const Something = component(counter => {
useWindowSize(counter, () => {
// something
});
});
Again, this isn't super hot, with custom hooks having a different signature from the built-in hooks... There's probably better way to do this, so yeah, it's just proof-of-concept at this point.
@mindplay-dk Thanks a lot for your answers. Actually I'm very interested in your ideas as your (quote) "little toy experiment" uses quite similar patterns like those I am using in some of my R&D projects.
See your example:
const Something = component(counter => ...)
If you rename what you've called counter
to c
and instead of passing the whole Preact.Component
instance just pass a facade/controller object as follows:
const c = {
props,
useState,
useContext
useEffect
useLayoutEffect,
...
}
then you basically have what I have called "c
thingy" above in that what I've called "simple factory pattern".
Again, this isn't super hot, with custom hooks having a different signature from the built-in hook
The simplest solution to this is to declare that "c
thingy" opaque (as done in ivi) and provide the following hook function out-of-the-box (the following is obviously a quick'n'dirty implementation):
export const useState = (c, ...args) => c.useState(...args)
export const useContext = (c, ...args) => c.useContext(...args)
export const useEffect = (c, ...args) => c.useEffect(...args)
export const useLayoutEffect= (c, ...args) => c.useLayoutEffect(...args)
Now you still have to decide how to handle the props
. There are a bunch of solutions of course.
I personally am not really sure whether it is really a good idea that hook functions have access to the current props. I have never seen one single example of a hook function where this is necessary (if there are good examples where hook functions need access to the current props - except for useProps
- then please let me know).
For example in your demo, the following ...
const Counter = component(counter => {
const [state, setState] = counter.useState(({ value }) => ({
count: value || 0
}));
....
}
... could easily be implemented as either ...
// `props` is a auto-mutating object here - always loaded with the current props,
// similar to Vue
const Counter = component((counter, props) => {
const [state, setState] = counter.useState({ count: props.value || 0 });
....
}
... or ...
const Counter = component((counter, getProps) => {
const [state, setState] = counter.useState({ count: getProps().value || 0 });
....
}
I personally prefer currently that the API of that c thingy
does not reflect the type of hook functions I will use later but is more abstract (you could write hook functions 100% like ivi does on top of that or hook functions similar to the patterns in your POC project)
const Counter = component((c: Ctrl) => {
const [state, setState] = useState(c, { count: 0 })
...
})
type Ctrl = {
// currently no access to the current props
update(runOnceBeforeRender?: () => void): void,
isMounted(): boolean, // sugar
getContextValue<T>(context: Context<T>): T, // I've cheated to make this possible
afterMount(subscriber: Subscriber): void, // `unsubscribe` not possible by design
beforeUpdate(subscriber: Subscriber): void,
afterUpdate(subscriber: Subscriber): void,
beforeUnmount(subscriber: Subscriber): void,
// ...
}
// without going into details: `runOnceBeforeRender` and `beforeUpdate` are
// a bit odd ... do not like them too much, but currently have no other idea
type Props = Record<string, any>
type Subscriber = () => void
type Context<T> = Preact.Context<T>
There are three main things I really love about the React hook API:
The possibility of writing hook functions, of course => this is obviously possible with the patterns that you and I are using above and also in a quite nice alternative way in ivi.
Except for ref objects with react hooks all data is immutable (at least in most cases): props
, count
, state
etc.
I do not know a way how this can be achieved with that "simple factory pattern" (without using getters all the time of course, but I would not like that too much).
This is really something I miss the most - this is sooooooo much better in React and ivi.
I really really hope, those guys working on that "reactive" solution will show me some day a reason, why this after all will still make really sense (and the whole things is not just a stopgap as JavaScript does not have PHP-like references or C-like pointers).
When you always define components the following way in React
function Counter ({
initialCount = 0,
title = 'Counter',
onChange
}) {
....
}
you alway see directly what properties are supported by the component (even without TypeScript) and also you see the default props directly - both without even have to provide some special API for that. This is sooooo amazing - I cannot express how much I like that. I have seen something like this only with React-like hook API and ivi (but it's nicer in React). I am missing this completely in all other APIs I have seen (especially that "simple factory pattern" I am using recently in some of my R&D projects).
@mcjazzyfunky yeah, a factory/builder proxy of some sort being passed, rather than than the actual component instance, that would make a lot more sense - I mostly did it that way to get to a functioning prototype quickly without adding another degree of abstraction. I also like your idea of having individual hooks (before/after/once) rather than compounding these like hooks do.
With regards to naming, I use e.g. counter
when I know I'm configuring the counter - if I write custom hooks, I use names like c
or component
, as those calls could be operating on different components.
I've never been fond of the use
prefix - the low-level built-in hooks are very different in nature from high-level custom hooks; custom hooks are a different layer of abstraction, so I actually think it's natural for them to have different naming/calling conventions. I would much prefer names like afterMount
, beforeUpdate
as in your example for the built-in hooks - in my own POC I mostly kept the names because it requires less work to explain it to someone who's already smitten with React hooks. 😊
Hi again,
for those of you who are still interested in alternative component API's and by any change have not yet heard of Google's "Jetpack Compose" project (for Android) => https://developer.android.com/jetpack/compose (not production-ready yet):
It uses Kotlin as programming language and applies a special Kotlin compiler plugin. With this combo you have of course much more possibilities as in ECMAScript and so some of the downsides of React's API can be fixed there. If not already done, check it out ... it really interessting :)
And then some other remarks: I've again noodled around a bit with some API alternatives for React hooks and actually I came to the conclusion (at least for myself) that maybe that dogmatic view that syntactical conciseness is one of the major goals is not very expedient. If the syntax is fine and more verbose than its React counterpart but not tooooo verbose then maybe that's still okay.
Also, I've never really been a big fan of using a useProps
hook (especially to handle default props) - even if I have used it from time to time in some of my examples above => I've changed my mind here: Actually without going into details I think using a useProps
hook is not a bad idea and using it to handle default props solves some subtle TS typing challenges (of course if you use ivi-like or react-like hooks in the first place you do not even have such problems nor need some useProps
hook function or whatever - but if you use some other alternative hook API it may be useful). Also using this useProps
function helps a bit to implement really all hook related stuff completely in userland.
Here's once again an additional example ... frankly I'm really fine with the syntax it is using (even if you may get used to it a bit): https://codesandbox.io/s/ecstatic-shape-p1uhx
What's important here is that the core of that toy library (module: "js-widgets") does currently only have six functions component
, context
, h
, render
, Fragment
, Boundary
(obviously portal
, Suspense
etc. are still missing, but that may not be very important here) and that this core API does not know anything about hooks
and therefore does not dictate how stateful components have to be implemented. It's just expects a ivi-like init
function the rest is up to you. It indeed provides a module "js-widgets/hooks" but if you prefer hooks the ivi-way or the React way and implement a corresponding module in userland the core of that little toy library will be completely fine with that.
Hi all, long time no see :) After 4 years I took some time to create https://github.com/zouloux/reflex. It seems to be working and have some conceptual changes from the original idea. After some tests and benchmarks (that I need to continue), I got better performances than Preact (less CPU and less memory usage) and library is less than 4kb gzipped (Preact for ex is 3kb but without hooks). It has a lot of stuff missing from React, (on purpose) but i'll check what to include next (like Suspense or Async rendering) I got some ideas inspired from Solid, which I find very neat (like the Proxy Props) and which also kind of have factory pattern. This is certainly useless and a crazy idea to re-create a V-Dom lib, but it was a very good exercise to do ! Still in very-very-early beta, doc and demos are not finished yet. See ya ✌️
@zouloux You're such a badass :smiley:, implementing all that stuff in reflex
- my deepest R.E.S.P.E.C.T! :+1:
Are you interested in some remarks (like always: It's totally fine to have a completely different opinion than I have :wink:)?
preact
hooks are not part of preact
's core, but in a different module, which is written completely in userland by using preact
's options
object for all the magic.reflex
, maybe in a submodule @zouloux/reflex/hooks
react
-like hooks in userland as an alternative to the common factory-based reflex
hooks. As the reflex
core would not know anything about hooks at all, both hook paradigms could perfectly live side by side, and reflux
users could decide on their own what hook paradigm they prefer.A simple counter demo that uses both hook paradigms at the same time (in different components, of course) could look like follows (btw: this setDefaults
hook tries to provide a solution for to the above discussed "default props" problem):
Please find here a dirty toy implementation of the above demo. Please ignore the fact, that the demo is build on crankjs
as UI library, that's not important at all (I could have used almost any other VDOM UI library as well). The function that is the base of all hook implementations is called getCtrl
and can only be called within the component's main function phase, otherwise an error will be thrown. It looks something like the following (I guess it's self-explanatory what that controller Ctrl
is doing):
function getCtrl(): Ctrl {
....
}
type Ctrl = {
afterMount(task: () => void): void;
beforeUpdate(task: () => void): void;
afterUpdate(task: () => void): void;
beforeUnmount(task: () => void): void;
refresh(): Promise<void>;
// some more in future
};
Hi @xdev1 and thanks for your feedback ! Yep I check how Preact deals with hooks as a separated sub-module and I'm not convinced by the option thing. Something like getCtrl seems a good alternative ! I also want to try a React hooks style implementation in Reflex but that's will not be into the v1, it will definitely be in a future version, as a separated sub-module, if I continue to maintain the lib. I did't checked about the setDefaults for props but it may be like early React version with something like :
Component.defaultProps = {
name: 'Unknown'
}
function Component ( props ) {
return () => <div>Hello {props.name}</div>
}
I think it will be easier for types and auto-complete and I find it clean enough (on top of component, not callable several times like setDefaults).
I think it will be easier for types and auto-complete
Honestly, I doubt that. How will you make the following example work properly in TypeScript (without adding a "!" or whatever everywhere you'll meet those annoying type issues)?
props.name
might be undefined
so you cannot just call props.name.toUpperCase()
and therefore this will not compile.
MyComponent.defaultProps = {
name: 'Unknown'
};
function MyComponent(props: { name?: string }) {
return () => <div>HELLO {props.name.toUpperCase()}</div>;
}
Neither in React nor in Solid (using mergeProps
) nor in my demo above you'll have those kind of type problems.
Please have a look at my previous demo again (I've updated it a bit, to better show that type issue):
Counter1
the type of props
will automatically be adjusted after setDefaults(props, ...)
has been called (otherwise count.value + 1
would not work) Counter2
(quite similar) props
has a slightly different type than p
(otherwise count.value + 1
would not work).BTW: Using MyComponent.defaultProps = ...
before function MyComponent(...) { ... }
might result in an ESLint error (=> @typescript-eslint/no-use-before-define
), depending on configuration of course.
Just defining the defaultProps
after the component function, will make the definitions of props and default props not be collocated any longer.
As a variant of my demo: You can of course define the counter default props right in front of the Counter
function definition (const counterDefaults = { ... }
) and use that with the setDefaults
or preset
functions, so that constant default props object will not be recreated unnecessarily again and again for each counter instance. But I personally do not like the DX that comes with that pattern, and so I personally prefer the following as an optional (!) syntax variation (this is not implemented in the demo above):
const props = preset(p, () => ({
count: 0,
label: 'Counter'
});
The function preset
will make sure that, in case the second argument is a function, this function will only be called once when initializing the first Counter
instance, and the result will be cached and reused for all other Counter
instances.
Some folks may call this "odd" or "too much magic" or "premature optimization" or whatever, but what do I care?! :wink:
Oh BTW, the most important question, IMHO: How are you planning to handle suspense in Reflex? If there's a nice syntactical way to handle suspense concerns in the component function, then great ... otherwise this may be a dealbreaker for the whole "factory-based component definitions" idea.
Hey, you changed my mind, I think you are right about the props defaults ! I'll check how to implement it because props is a Proxy object. Now it's a different proxy by component, but for performances purpose, I'll provide the same Proxy instance to all components, and the proxy will dispatch props automatically.
For suspense, I see no issue about implement it in Reflex. I need to propagate an env
object through nodes like petit-dom
does it. For now Reflex does not support SVG, and it's needed for SVG so it may come at the same time. It's not planned to use the same API, I personally does not like the <Spinner />
as prop, I need to check if something cleaner is possible. Also looking into implementing useTransition
and useFetch
as separate package !
And thanks for your feedbacks 🙌 ! Feel free to create some PR if you want to be involved, dirty code is OK since code review.
[...] for performances purpose, I'll provide the same Proxy instance to all components, and the proxy will dispatch props automatically.
Before you waste time on that, I don't think that this will work. You will indeed need a separate props proxy for each component instance, because using always the same props proxy will fail in a lot of async cases.
See for example this very realistic :wink: demo component. To access props.value
properly there, you really need separate props proxies.
function Demo(props: { value: string }) {
mounted(() => {
const intervalId = setInterval(() => {
console.log('Current value:', props.value);
}, 1000);
return () => clearInterval(intervalId);
});
return () => <div>Please check JS console...</div>;
}
Are you a wizard ? Already lost some minutes on this but it was easy to recover ! Thanks ✌️
New version with defaultProps
, svg support, renderToString 🎉
https://github.com/zouloux/reflex
Plus some performance improvements, it seems faster than Preact now with a bit less memory usage https://github.com/zouloux/reflex#performances
@zouloux May I ask you to provide a little demo in TypeScript where you use default props with Reflex?
Yep :) It should be something like this :
interface IComponentProps {
title ?: string
}
function Component ( props:IComponentProps, component:IComponentAPI<IComponentProps> ) {
// Works on factory and functional components
component.defaultProps = {
title: "Default title",
notExisting: true // Rises a TS error
}
console.log("title", props.title) // "Default title"
return <h1>{ props.title }<h1>
}
For now defaultProps
type needs to be given through component generics, but I hope to find something to automate this like JSX runtime types.
IComponentAPI
will have other utilities like imperative handles, component forwarded ref. Also shouldUpdate
works with IComponentAPI
-> component.shouldUpdate = (n, o) => n.title !== o.title
@zouloux Many thanks ... but again props.title.toUpperCase()
will not work, as props.title
may still be undefined.
Sorry but I do not get how props.title
could be undefined
? Can you tell me more ? I can try some more real-case examples quickly. Thanks a lot :)
Sorry but I do not get how props.title could be undefined ?
Sorry, my bad ... I've used the wrong wording.
I meant: The type of props.title
is still string | undefined
and TypeScript will throw an compile-time error saying "Object is possibly 'undefined'". While in reality props.title
will not be undefined
any longer, as a default value has been defined for prop title
, but TypeScript doesn't know that.
I'll prepare a little demo ... stay tuned :wink:
@zouloux Here's the demo: https://codesandbox.io/s/default-props-39wquo?file=/src/index.tsx [Edit] And don't tell me the upper case should be done by CSS :smile:
Aaaaaaaaaah the strict mode in tsconfig ... Yep that's an issue I did not think of ! I didn't catch you were talking about types and not values here, but I should take care of what you are saying because you are right again :) I'll search for a solution but the preset
hook is maybe the only way to have this working in strict mode. Can't destruct in function arguments, can't use IComponentAPI
to propagate types back to GProps
generics, or if there is a way, I do not know it. Thanks again ✌️
@zouloux Here's a bunch of assorted notes/remarks/ideas (some of them highly opinionated)
I think, you should activate discussions in your reflex
project :wink:
It would be really helpful, if the demos in reflex
's README.md
were written in TypeScript or at least a bunch of non-trivial examples.
Also, using prettier
would be very nice. Every syntax that is used with the reflex
patterns should work fine with prettier
, as a majority of developers (including me :wink:) use prettier
wherever possible.
I would consider, not to use the name hooks
anymore if it's not "react-like" hooks
. Folks will get confused otherwise (especially those who do not like react-like hooks, I know this from own bad experience in other discussions :smile:). Maybe extensions
could be a good name instead, as those functions are conceptionally extension functions where the first argument (= object) will just be passed implicitly.
One major goal for that factory-pattern based components should be to increase conciseness wherever possible and reasonable. In the old days we used stuff like this.props.label
which was extremely cumbersome, React now uses just label
which is awesome, while reflex
uses props.label
, which may look okayish first, but if you love React as I do and then you use reflex
(or something similar), then even props.label
seems way to noisy. I personally really prefer using the variable name p
instead of props
which reduces noise at a good amount especially in larger components. I know, normally we try to prevent single-character variables (except for maybe i
, j
, x
, y
etc.) but here this shortcut makes perfectly sense, IMHO. By the way, I'd also use s
for a state object (i.e. s.count
instead of state.count
) and c
for the used context values (i.e. c.theme
).
Regarding stateless components: I would not provide some special API for those stateless components as it is not really necessary.
If the "normal" react
ish way looks like:
type GreetProps = {
name: string;
salutation?: string;
};
function Greet({
name,
salutation = "Hello",
}: GreetProps) {
return (
<div>
{salutation}, {name}!
</div>
);
}
they can use alternatives that look a bit more similar to stateful reflex
components if they like (this syntax is a little shorter than the above one):
function Greet(p: {
name: string;
salutation?: string;
}) {
const {
name,
salutation = 'Hello'
} = p;
return (
<div>{salutation}, {name}!</div>
);
}
Some more will follow in a separate comment ...
(... continuing list from latest comment ...)
count.set(count.value + 1)
shall be better than setCount(getCount() + 1)
or setCount(count() + 1)
or count(count() + 1)
I'll search for a solution but the preset hook is maybe the only way to have this working in strict mode.
The only type-safe solutions that I know are the following:
// React version for comparison
// lines: 18, chars: 341
type CounterProps = {
name: string;
initialCount?: number;
};
function Counter({
name,
initialCount = 0
}: CounterProps) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(it => it + 1);
return (
<button onClick={increment}>
{name}: {count}
</button>
)
}
// This the shortest version of my proposals above:
// 17 lines, 342 chars
function Counter(p: {
name: string;
initialCount?: number;
})
preset(p, () => ({
initialCount: 0
}));
const [getCount, setCount] = stateVal(p.initialCount);
const increment = () => setCount(it => it + 1);
return () => (
<button onClick={increment}>
{p.name}: {getCount()}
</button>
);
});
/*
This curried `component` function may not have
a very popular syntax, but is actually surprisingly
concise and can be implemented on top of most
modern vdom-based component libraries.
In the following form the component does not
have a "display name", if needed for debugging etc.
it has to be added automatically in the build process
or declared explicitly:
const Counter = component('Counter')<{
...
});
*/
// 15 lines, 329 chars
const Counter = component<{
name: string;
initialCount?: number;
}>({
initialCount: 0
})((p) => {
const [getCount, setCount] = stateVal(p.initialCount);
const increment = () => setCount(it => it + 1);
return () => (
<button onClick={increment}>
{p.name}: {getCount()}
</button>
);
});
[Edit]: Here's an additional alternative API based on a lot of function compositions (may look a bit unusual first, but is quite flexible)
const Counter = component('Counter')(
props<{
name: string;
initialCount?: number;
}>,
defaults({
initialCount: 0
})
)((p) => {
const [s, set] = state({ count: p.initialCount });
const increment = () => set.count(it => it +1);
return () => (
<button onClick={increment}>
{p.name}: {s.count}
</button>
);
});
Of course in this simple example just using p.initialCount ?? 0
would have also worked, but the purpose was to show general "default props" patterns.
From all those proposals from RFC this one definitely looks the tidiest :) I am curious how you would approach custom hooks? In my opinion, these are what makes React hooks the most brilliant - ability to separate and share encapsulated behaviors.