react-factory-hooks
react-factory-hooks copied to clipboard
Propsal: react-factory-hooks - General Discussion
Hi, feel free to comment what you think about the proposal. This should help to keep RFC: React Hooks #68 from making so much noise.
Unfortunately, I did not have much time to write the proposal together at all. Therefore, many parts are not well explained or illuminated, but everyone is free to provide a PR with suggestions for improvement or the like.
Thank you very much @PutziSan for sharing your ideas, thoughts and opinons with the community. May I ask you whether you have by any chance a little demo of your proposal?
Hi, you found something that wasn't finished yet ;-)
I currently update the final proposal with readme and then would add a comment to the discussion under https://github.com/reactjs/rfcs/pull/68
@PutziSan I'm still puzzled why there is hardly no real discussion on alternatives to React's proposed hook approach. To share and discuss different ideas and approaches is one of the major fun parts in our job, isn't it? .... just wondering... Anyway, like I've already written in that large discussion thread, I think if we are using classes and we want to get rid of that "classy" approach, a factory is something that has at least a change of being a good alternative. Many thanks for your great efforts to present/demonstrate details on your corresponding idea. What I like most about your proposal is that there's no hook order dependency. Does also make some other things easier. One main disadvantage is obviously that a factory approach is more verbose - conciseness is really the big win of the React's current hook API IMHO (next to custom hooks of course).
Regarding testablility: Frankly, I'm not really convinced that your approach (same for most other alternative approaches) will really simplify testing. I think it's mentioned in several comments and articles that maybe it's not the best idea to test component internals (for example if you have already written a lot of class based components and you want (for whatever reasons) to convert them to function components using the new hook API, I'm quite sure you would be happy if you did not have to rewrite all that unit tests for all that components). And I'm also sure if you want (for unit testing or whatever) better isolation of pure logic from steateful/effectful logic, that with some imagination you'll find a way to develop some fancy custom hooks or whatever that will help you there.
Last but not least just my personal opinon about React's new hook API:
- Do I like the basic concepts/ideas? Yes I think that they are really, really smart...
- Do I like the downsides? Of course not ....
- Do I really have practical problems with those well-known downsides of the current hook API in daily work or are those "problems" just of a theoretical nature? Frankly, I don't think that there will be real practical problems .. almost everything will be easier and be implemented more quickly as before ... and I really think, this should be the most important concern, shouldn't it?
As someone who was for a long time of the opinon, that a factory pattern could be a good alternative, I really have to admit that the following argument that has been mentioned in that large "react hooks" discussion thread really seems to be a showstopper for the use of a factory pattern or similar approaches (thanks to everybody who made this downside clear):
@FredyC said (2018-11-18)
@PutziSan I believe that this comment from @sophiebits pretty much covers most of your proposal. Having access to props in a custom hooks feels awkward to me as suddenly you are tightly coupled to a specific shape of the object.
Needing a useProps hook instead of being able to rely on arguments is a little unfortunate IMO. Not only does it feel more complicated (to me, at least), but it has two other quirks: first, it breaks encapsulation for custom hooks (today, we don’t have useProps in part because it’s nice that a custom hook can only access parameters passed to it; it can’t observe or modify the component calling it) [...]
[Edit: Maybe the word "showstopper" was a bit too hard, of course there are fancy solutions to use prop values inside of a custom hook without using "useProps" in those custom hooks - but things are getting more and more complicated then and at maybe the overall syntactical overhead will be the "showstopper" in the end]
Thank you for sharing your thoughts @mcjazzyfunky and for @FredyC pointing this out.
I agree that it forces developers to write hooks independently of components when a hook by default has no access to props
. And this is a very good feature of the hooks-proposal, where my suggestion couldn't keep up.
@zouloux proposed a nice solution for this in the large hooks-rfc-discussion and published his idea under solid-js/prehook-proof-of-concept.
I have incorporated his suggestion into this proposal and adapted the README accordingly. My suggestion now differs in that I would still add props
as a parameter in the render function. I have described the reasons for this in the readme under Why the render function should get props
as argument. Feel free to share your thoughts about this.
I still believe that the solution with a factory function is much clearer than it is currently proposed.
@PutziSan Regarding that "props" argument in the returned render function: That's obviously syntactical sugar. I compare it the the "props" argument in Preact's Component#render(props, ...)
method. It helps a bit to transform a stateless render function to such a factory-based render function and it allows shorter syntax in some cases, so why not? => in the end it's just a matter of taste, IMHO..
function MyFancyButton(getProps => {
...
return ({ text, className, onClick }) => {
...
}
})
The goal of the following question is not to convince anybody of anything, it's just the hope to gain some knowledge myself by reading the answer :-)
@PutziSan
I still believe that the solution with a factory function is much clearer than it is currently proposed.
That means the following example (that I gave in the other thread) is not a showstopper in your opinon regarding syntactical noise, additional type complexity (in TS) and increasing debugging efforts. May I ask why not? (pardon, if that question sounds kinda odd - but you'll meet that pattern over and over again, I think)
// current React hook API
const [width, height] = useWindowSize()
useFancyHook(`The window size is ${width}x${height}`)
// some API based on factory pattern
const [getWidth, getHeight] = useWindowSize()
useFancyHook(() => `The window size is ${getWidth()}x${getHeight()}`)
@mcjazzyfunky, thank you for your interest. I've just written a third draft including a revised README, which also deals with your questions (I think). If you like, you can have a look at the new FAQ.
@PutziSan Well, you got somewhat closer, but it's not it and I am not longer convinced this can be done with factory pattern while keeping advantages of the official hooks. Feels like instead of reducing "unintuitive" rules you have added a bunch of more :D
I haven't read a whole doc, but some things stood out
- useEffect not being able to do in a component and have to use class 😮
- custom hooks are used within a component body and regular hooks in factory part? 😕
- removed useRef? The createRef is not mutable...
Edit: Also regarding custom hooks ... so you cannot initialize state with a value passed from the component? 👎
@FredyC, thanks for your questions, I will try to answer:
useEffect not being able to do in a component and have to use class 😮
You can use useEffect
inside a functional component, like:
function App() {
const docTitleEffect = useEffect(name => {
document.title = `Hi ${name}!`;
});
return props => {
docTitleEffect(props.name);
return <div>your component</div>;
};
}
But I think it is more intuitive for such basic usages to use a react-component:
function App(props) {
return (
<>
<UseEffect
effect={() => document.title = `Hi ${props.name}!`}
/>
<div>your component</div>
</>
);
}
custom hooks are used within a component body and regular hooks in factory part? 😕
Thats not completely true, you have to init your useEffect
in the factory-function and then you can use your defined effect in the render-part: (see first example in this answer)
removed useRef? The createRef is not mutable...
You can just use a normal variable (but maybe i'm missing something, cause the use-case is not realy clear to me)
function Example() {
let ref;
return props => {
ref = props.name;
return <div />;
}
}
Also regarding custom hooks ... so you cannot initialize state with a value passed from the component?
you can just use the initialProps
:
function Example(initialProps) {
const getIsFriendOnline = useFriendStatus(initialProps.defaultOnlineState);
return props => {
const isOnline = getIsFriendOnline(props.friend.id);
// ...
}
}
You can use
useEffect
inside a functional component, like:
Well, in that case, you should make it as not recommended as it leaves a reader puzzled why. Leave your personal preference out of it please :)
you can just use the
initialProps
:
I am talking about custom hooks, these don't have initialProps (and should not have).
Well, in that case, you should make it as not recommended as it leaves a reader puzzled why. Leave your personal preference out of it please :)
Good point, thx. I will change that.
I am talking about custom hooks, these don't have initialProps (and should not have).
I'm afraid I don't understand exactly what you're suggesting. So according to this proposal:
- a custom hook has no access to
props
orinitialProps
during initialization - but you can call your custom-hook with parameters from
initialProps
during the factory-phase
To specify the example of a custom hook of my previous answer:
// my custom hook:
function useFriendStatus(defaultOnlineState) {
const [getIsOnline, setIsOnline] = useState(defaultOnlineState);
// ... handleStatusChange
const friendEffect = useEffect(friendId => {
ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange);
};
});
return friendId => {
friendEffect(friendId);
return getIsOnline();
};
}
function Example(initialProps) {
const getIsFriendOnline = useFriendStatus(initialProps.defaultOnlineState);
return props => {
const isOnline = getIsFriendOnline(props.friend.id);
return isOnline ? "online" : "offline";
};
}
Ok, I've missed that part about custom hooks, makes sense now.
I guess it's alright proposal, although it still suffers the issue of default props. With this, you would need to specify defaults for initialProps
and from props
as well. The only approach that might work would require a function that gets a props object and returns a new one with defaults filled in but that's already annoying just thinking about it :)
Have fun with your little project. I don't see any point in pursuing this anymore. I am using current hooks in a production and I am happy with those. Those rules people are still complaining about feels quite natural after some time. I don't see any sort of magic happening there. Everything makes sense and it's easy to use and reason about.
@FredyC thank you very much for your effort, your thoughts and your ideas! It is clear to me that these proposals will not be adopted by the react team, as their proposal is quite popular (but also controversial). It was still interesting for me and maybe for others to see what happens when you finish thinking about the factory idea.
For me, the result is that it is definitely possible to have a similar development experience to the react proposal in this way.
I will add one last comment in the big RFC-discussion to share my insights for those who find it interesting.
ps: I added your last hints to the current version of the documentation ("not recommended" removed from useEffect
).
pps:
With this, you would need to specify defaults for initialProps and from props as well.
Yeah, that's what I had in mind. But I never had the case with my projects that I needed a `prop`` to initialize and then later worked with it in the actual UI. In most cases it's more like that:
function App({ initialValue = 'foo' }) {
const [getValue, setValue] = useState(initialValue);
return ({ otherProp = 'bar' }) => (
<div>
<p>{getValue()}</p>
<p>{otherProp}</p>
</div>
)
}
Which is why it's more of a rare case that there are problems having to set it twice.
Had nearly the same thoughts on using closures for hooks execution. To me this felt great also because there are much less ways to affect render performance using such approach for hooks. Unfortunately looks like this is also the main disadvantage. Seems having hooks running on each render call simplifies a lot of things.
Anyway, thanks for sharing this. Great work @PutziSan
@PutziSan I guess you maybe interested to look at ivi for some inspiration in your proposal, I've implemented () => (props) => vdom
API on the next day after React hooks announcement.
But I never had the case with my projects that I needed a `prop`` to initialize and then later worked with it in the actual UI.
Easily solvable by specifying function as a state constructor that derives initial state from the first invocation to the state pipeline.
Inability to conditionally use useMemo()
with React hooks API is way much worse and is a more common use case, in some conditions we just won't have values to pass into useMemo()
, but we still need to increment internal state index.
@PutziSan in explanation of #4 Why we need global use* methods? I found "functional DI" works simpler and cleaner: https://codesandbox.io/s/01z7mz90rv
@artalar
Why we need global use* methods? I found "functional DI" works simpler and cleaner:
So that custom hooks can be created and used as intuitively as possible, they should work just like the basic hooks.
However, this is rather hacky, as I briefly indicated in #4. To get away from the global variables we have to pass a local variable via the HOC, your idea is quite good, but makes creating custom hooks a bit more difficult. Also, when you implement an effect, it can't access the current props. In "skipping effects" I show how this could be possible with the factory pattern.
You could customize your code to be a consistent API like:
const Component = factory((ref /*, initialProps*/) => {
const [getState, setState] = useState(ref /*, defaultValue */);
const myEffect = useEffect(ref, name => console.log(name));
return props => {
myEffect(props.name);
return <p>{props.name}</p>;
};
});
and with a custom hook:
function customHook(ref, initialStatus) {
// adding a nested function is necessary to get the use-param
const [getIsOnline, setIsOnline] = useState(ref, initialStatus);
const friendEffect = useEffect(ref, (friendId) => {
ChatAPI.subscribeToFriendStatus(friendId, () => setIsOnline(true));
});
return friendId => {
friendEffect(friendId);
return getIsOnline();
};
}
const Component = factory((ref, initialProps) => {
const getIsFriendOnline = useFriendStatus(ref, initialProps.status);
return props => {
const isOnline = getIsFriendOnline(props.friend.id);
if (isOnline === null) {
return "loading...";
}
return isOnline ? "Online" : "Offline";
};
});
Unfortunately, accessing props in effects is not very intuitive and that is a clear weakness in my proposal.
@PutziSan
Thanks for your answer!
I was thinking about API and talks with colleagues and found very useful that options: createHOC(use => use(hook))
- it gives us the possibility for ease controls hooks: for middlewares, logging, mocking, etc. I was updating readme and examples, you can find it here..
Unfortunately, accessing props in effects is not very intuitive and that is a clear weakness in my proposal
For that, I was adding getInitial
API.