-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Hooks #87
Comments
@thysultan Really great that you are planning to add a hook API to Dyo. Thanks. Just a comment to point 1: Please see the following example: // React - everythings completely understandable here
function Counter(props) {
const
[count, setCount] = useState(0),
{ label, onChange } = props,
prevLabel = usePrevious(label),
prevOnChange = usePrevious(onChange),
prevProps = usePrevious(props),
prevCount = usePrevious(count)
...
}
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
} // Dyo - things are a bit confusing here, I think...
function Counter(props) {
const
count = use(0)
{ label, onChange } = props,
prevLabel = usePrevious(label),
prevOnChange = usePrevious(onChange), // onChange is a function
prevProps = usePrevious(props),
prevCount = usePrevious(count) // count is a function
...
// Some confusing things
// * "count" is a function - is "prevCount" is a number or a getter function?
// You cannot guess the type of a value by its name any longer.
// * How can "usePrevious" know that "onChange" is a function
// which shall be treated as a value while "count" is a function
// which shall be treated as a getter function? Means, "usePrevious" is
// not as easy to write and not as easy understandable as in React...
// Of course you can just use: prevCount = usePrevious(count()) ... but still confusing...
}
|
@mcjazzyfunky From the example i can't pin point in what context this would be a problem. Can you elaborate? i.e how are you using the values, and how is this different from React. As an aside if for example In addition, assuming the tuple approach As it stands dependencies are single live values so My reasoning was that this would widen the breath of compostability afforded to effects in addition to the aforementioned note about closures in point 4. |
@thysultan Like I've said I'm just "not really sure" ... maybe my concerns are a bit overstated. But what's really a bit odd in my opinion is the following: Let's say again at a certain point in the render function you have that variable"count". If it comes from "use" it's a stream if it comes from "props" it's a number. Should the source of data really be THAT important (different types depending whether it comes from state, from props or from context)? If the data source is state than you do NOT have that "stale data" problem and you can pass the data stream easily to async functions. But if the data source is "props" or context then your STILL have that "state data" problem and cannot pass the data that easily to async functions, am I wrong? (maybe some "useStream(...)" hook could be a solution). But really no need for a larger discussion just because of that "value vs getter/setter-stream" thing ... if you use "getter/setter-streams", I'm fine with that of course 😃 Regarding that |
My experience with getter-setter functions (working with Mithril.js years ago) was poor.
I'd like to note that |
You could type define it as: interface Use<T, Props = {}, State = {}, Context = {}> {
(value: T, callback?: (Value<T>[], Props, State, Context): (void | (): Promise<any>))
}
@Zolmeister Yes, React uses the second argument to signal dependencies, where implicitly it is an empty array(no dependencies). In contrast this forces you to explicitly mention dependencies if there are any, which is either a pro(is explicit) or con(is explicit?) depending on what you want to do.
I agree. |
I can also see benefits to re-ordering the arguments for Instead of merging Edit: ah, I take it back |
I agree overloading Basic Hooks
Additional Hooks
|
|
I think we can ship the following primary and secondary hooks: Primary
Secondary
It's debatable whether we should include All of the above are implemented in the hooks branch. |
@thysultan What about |
I was mistaken as to it's purpose, that said ...The function served by |
I think |
Interesting. I think useRef is versatile enough as a primitive(at least in Dyo) that you can do this with the function initialiser pattern. For example: // Dyo
function FancyInput(props) {
const ref = useRef(props => {
props.ref.current = {focus: () => ref.current.focus()}
})
return <input ref={ref} ... />
} In React this would be the equivalent of. // React
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput); Is there anything i'm missing? |
@thysultan Ooooops, sorry for accidentally closing the issue... |
The main reason i can see React still removing "ref" from props is from class components where passing a ref to a class component in react would invoke the ref with the instance of the class first, this meant that you would either have to live with the ref getting invoked twice once for the component instance, then the DOM node, or remove the ref from props to avoid this, i might be wrong but this is a similar situation in which DIO was in previously. Going all-in with function components makes this a none-issue since instances effectively don't exist. Dyo will take liberties where it makes sense but is still at heart very close to a "React-like" API, i.e similar examples include altering the naming of |
Even if those cases may be very rare, to have Oh, and thanks for the details about the API differences to React. Means, my above asked questions are all answered now. Thank you very much. |
The only prop i can see being potentially destructive is "key", though considering that if the parent that's passing the key, changes the key, then the parent would be destroyed in either case. The only other issue might be in the order of de-structuring when you are suppling a key i.e Where there any other scenarios you had in mind? |
I think we will also have trouble in TypeScript, as soon as the Just assume that someone has written type CounterProps = {
initialValue?: number,
label?: string
}
// We ignore this type information here (as it is not really that important here)
// type CounterMethods = {
// reset(n?: number)
// }
function Counter(props: CounterProps) {
useImperativeHandle(
props.ref, // <------------ TYPE ERROR !!!!!!!!!!!!!!!!!!!!!!!!!
() => {....} // add imperative function 'reset(n)' here
)
....
} ----- Edit -------------------------- While a properly typed solution could look like: type CounterProps = {
initialValue?: number,
label?: string
}
type CounterMethods = {
reset(n?: number)
}
function Counter(props: CounterProps, ref?: Ref<CounterMethods>) {
useImperativeHandle(
ref,
() => {....} // add imperative function 'reset(n)' here
)
....
} |
Is there any reason that the following amendment wouldn't alleviate that TypeScript error? type CounterProps = {
initialValue?: number,
label?: string,
+ ref: Ref<CounterMethods>
} |
Maybe I'm completely biased regarding that Nothing of that is a big deal and all of the very rare above mentioned issues can be handled easily in some way, but nevertheless it does not feel like an especially good solution to me. Hope the following example is not too confusing, but at least it's real world example: https://stackblitz.com/edit/react-ts-wdp64s Be aware that To make a long story short: If |
I can see this going both ways, i.e That there's an inconsistency divide in declaring I can understand why this was done in the presence of class instance refs but conceptually a pass-through heuristic appears less magical than the runtime explicitly deleting these props and introducing API's to deal with the issues this might cause for some types of components, a thing we've previously seen mitigated by runtime heuristics for example |
Okay, then handling of |
I'm hoping that goes into the same isle of "consider props to be immutable" – "always put destructing first". Which consequently is identical to how it would work with plain JavaScript objects |
Batching out of band updatesConsider the following: const Example = props => { console.log('render')
const [state, dispatch] = useState(props)
// example 0 (batched)
useEffect(props => {
dispatch({children: 'Hello'})
dispatch({children: 'Hello World'})
}, [])
// example 1
useEffect(async props => {
await timeout(1000)
dispatch({children: 'Hello'})
dispatch({children: 'Hello World'})
}, [])
// example 2
useEffect(props => {
setTimeout(() => {
dispatch({children: 'Hello'})
dispatch({children: 'Hello World'})
}, 1000)
}, [])
return state.children
}
render(Example, target, (current) => {
console.log('complete')
}) As it stands boy React and Dyo would only batch In addition the log What should we do?
|
2 seems valuable, +1 |
Event iterables.Considering we can attach multiple event listeners to a single event with arrays: return h('button', {onClick: [a => 'Hello', b => dispatch(e)]}, 'Button') What model should we opt for.
Number 1 is what is currently implemented in Dyo(DIO used heuristic 2) but i'm questioning what is the cost-benefit between the two. |
I didn't even know it was supported. |
@Zolmeister In it's current form the implementation details of That is // render 1
const fn = useCallback(event => event, [true])
// render 2
const fn = useCallback(event => event, [false]) Event though the dependencies values have changed, would always return the same function, but still update the pointer to the callback that is invoked when called. So a sort of best of both worlds.
Yes the is one side-effect of this, we could push them to the end to solve this? function handleClick (event, props, [state, dispatch]) {
dispatch(state.a + props.b + event.type)
}
// ...
useCallback(handleClick, [state, dispatch])
useCallback(event => event)
Any user-land solution would rely on always creating closures on every render. When you're optimisation the final straws of your app there may come a time when this is where you might want to exert your final efforts, being able to archive this when required is an equally strong motivation behind this. |
I don't understand how your example works. How does it know what |
I updated the example(there was a error with missing dependencies tuple). That said the The callback returned from I'm sure you can probably re-create this with a user-land solution(using useState, useRef, useMemo) that avoids the "creating closures on every render" clause. But for something as primitive as this it should probably be part of the library. |
I'd be in favor of removing the function fn(a, b, c, e, f) { }
cb = useCallback(fn, a, b, c)
cb(e, f) |
In practice this is what it does. cb = useCallback((a, b) => { console.assert(a + b == 3) }, 1)
cb(2) However it doesn't allow arbitrary argument arity. Which is why a tuple was used to mimic arbitrary arguments. cb = useCallback(([a, b], c) => { console.assert(a + b + c == 6) }, [1, 2])
cb(3) |
Why not support arbitrary argument arity? |
When considering the common case of attaching the callback as an event, handlers are passed In the case of pinning them to the head( In the case of pinning them to the tail, it's ergonomically not possible if we still want |
SuspenseIn conjunction with Exhibit A: function Example (props) {
const data = useResource('/details')
return <pre>JSON.stringify(data)</pre>
}
function useResource (url) {
const [data, dispatch] = useState(null)
if (data === null) {
throw fetch(url).then((res) => res.json()).then(payload => dispatch(payload))
}
return data
}
render(
<Suspense fallback="Loading...">
<Indirection>
<Example/>
</Indirection>
</Suspense>, document) At the moment you could probably implement this with error boundaries, but any error boundary between you and the consumer would be able to intercept this propagation. Special casing throwing promises/thenables as a special contract to talk to |
Alright, I am in favor of passing the second argument of I like the idea. Can it be done without throwing? e.g. via hook? |
Maybe generators but would be more involved and go against the spirit of hooks. @mcjazzyfunky Related to the |
@thysultan Many thanks for that React RFC link - indeed really interesting (seems like you have some kind of fortunetelling crystal ball 😄) |
@thysultan When I saw Dyo's way of handling context and error boundaries with hooks https://overreacted.io/why-isnt-x-a-hook You find the following line there:
Actually Dan has a good point. |
@mcjazzyfunky I disagree. Conceptually That is to say I would like to be proven wrong though if you have a case where that is true for |
Just assume you have the following code snippet (where function Outer(props) {
useYellowTheme()
useSomeMysteriousCustomHook()
return h('div', null, props.children)
}
function Inner() {
const [theme] = useContext(ThemeCtx)
return `Used theme color: ${theme}`
}
render(h(Outer, null, h(Inner)), document.getElementById('app')) Then you have a look at the output and you see:
This is not what you would expect. Here is the full demo: |
What you would except depends on different factors. Firstly if we assume that function Foo (props) {
const value = useCustomValue(1)
return h('div', null, value)
} In which case So for either Now instead assuming Today in React any component with a reference to the provider above it could alter the value of the theme. function Outer(props) {
useYellowTheme()
useSomeMysteriousCustomHook()
return h('div', null, h(ThemeCtx, {value: "red"}, props.children))
}
render(h(Outer, null, h(Inner)), target) So in that case we can expect something different as well, assuming the base assumption was that the value should be
Yes that was a bug, published 0.0.22 – https://unpkg.com/[email protected] which i assume will eventually propagate to https://unpkg.com/dyo as well. |
@thysultan Maybe I am wrong, but I just cannot think of a case where two well-designed (= without any evil stuff/unexpectable side-effects) custom hooks in React are not composable. With Dyo, if you have some |
If it's possible in React it should be similarly be possible in Dyo and vice-versa, as the API contract is in-parallel to
You could still archive the same with React |
My interpretation is that you would never use function MyComponent(){}
MyComponent.componentDidCatch = _=> h('failed') function MyComponent(){
return componentDidCatch(h(Throws), _=> h('failed'))
} @thysultan is there an example of a custom hook that would use |
A For example
|
I see |
The const useErrorReportingService => useBoundary(exception => {
fetch(`post/to/error-reporting-service/${JSON.stringify(exception)}`)
})
const Example = props => {
useErrorReportingService()
return props.children
} |
Not a very compelling example (the error reporting function is more correctly written purely, as below). Can you provide a non-trivial example? (One where the const Example = props => {
useBoundary(errorReportingService)
return props.children
} Why not make it a component like |
How would you for example do the fore-mentioned with a |
Yes, like suspend |
|
|
Per latest commit, The fallback can be anything you can render (function/array/element/etc). When it is a function the exception object is passed to it. <Boundary fallback={<h1>Error</h1>}>
<Boundary fallback={props => JSON.stringify(props)}>
<Boundary fallback={[props => fetch('report/...'), props => JSON.stringify(props)]}> |
Similar to React Hooks. RFC discussion.
The differences being
value = use(value)
instead of[value, setValue] = use(value)
. This allows us to use live values to avoid the age old problem of stale data. That isvalue()
would always return the current value.use
API. i.euse(value)
anduse(value, callback)
instead ofuseState
anduseEffect
.componentWillUnmount
. Without which hooks wouldn't have full feature parity with lifecycle methods.[dependencies], props, state, context
to the effect callback to effectively avoid being tied to closures.An example of the aforementioned might look like the following:
Point 3 arises the question of how do we handle the case when multiple effects try to register unmount animations: Do we 1. Register all the animations? or 2. Register the last animation?
The text was updated successfully, but these errors were encountered: