-
Notifications
You must be signed in to change notification settings - Fork 7
Description
In React: https://reactjs.org/docs/hooks-reference.html#useimperativehandle
Usage in Muban: As a last resort when using props doesn't work.
Example: setting the focus of an input element wrapped in a component.
Questions to answer:
- Child – In React the
imperativeHandleis attached to the passedrefusingforwardRef. In Muban we have nor need either of those currently. So how do we "expose" the handle to the outside? - Parent – In React, the
imperativeHandlewill be attached to the passedref, and can be used after the first render (after the useEffect). In Muban, we we either have to pass something (like a built-inrefprop), or we can "retrieve" the handle from therefComponent(just like we extract props).
Suggestions
1. using bindings
Adding a custom forwardRef param that can be passed to a bind.
// parent
const handleRef = ref<TypeOfHandle>();
watch(handleRef, () => {
handleRef.value.focusInput();
});
// this needs to be a custom propType so we can pass a `ref` (instead of a `computed` that gets `unwrapped`
bind(refs.child, { forwardRef: handleRef });// child
// depending on timings, the forwardRef might not be passed from the parent yet
// so internally we would have to wrap it inside a computed, and double-wrap it to not auto-resolve
setup({ forwardRef }) {
// this will attached it to the `forwardRef` `ref`
useImperativeHandle(forwardRef, {
focusInput() {
// implementation details
}
});
}2. using the refComponent
This setup is a bit more integrated, and doesn't require a user-defined ref to pass along.
The (only?) downside is that it doesn't let you watch for when the "imperativeHandle" is available. Although in almost all cases I don't see this as an issue, as you won't use them immediately in the setup function anyway, but rather on different async events.
// parent
setup({ refs }) {
// Depending on the timing, this might not exist yet.
// Otherwise the `useMounted` might need to be used, or any other "async handler".
// For lazy components this might be more unclear when it can be used?
refs.child.component?.imperativeHandle?.focusInput();
// or even just:
refs.child.component?.focusInput();
}// child
setup() {
// this gets registered internally
useImperativeHandle({
focusInput() {
// implementation details
}
})
}2.5. providing the useImperativeHandle from the setup params, so it can be typed
This setup has the same "usage", but a more integrated "register", to better make used of types (see further below).
// child
setup({ registerImperativeHandle }) {
// this gets registered internally, and it type matches that of the component
registerImperativeHandle({
focusInput() {
// implementation details
}
})
}Challenges
Timing
Since the "imperativeHandle" contains runtime code that could interact with objects inside the setup function, it must be "registered" there as well.
For 1️⃣ it would make life easier of the parent was executed before the child, but that's not the case. So both the user and the framework implementation is more cumbersome.
For 2️⃣ it would make life easier if the setup function of a child component is called before the setup of a parent component, which should be the case. So to me, that sounds like the best option for multiple reasons – user and framework simplicity.
Typing
Most of the component's types can be inferred/extracted from the object definition (the refs and props). But the "imperativeHandle" would be defined in the setup at "runtime", and cannot be used automatically.
For this we would most likely need to provide additional types to the defineComponent and the useImperativeHandle.
type FooHandle = {
focusInput: () => void;
};
// specify here to add to the component type
const Foo = defineComponent<FooHandle>({
setup({ registerImperativeHandle }) {
// this is already typed, so the passed object must match the `FooHandle` specified for the `defineComponent`
registerImperativeHandle({ ... });
}
});Naming
Having imperative in there is probably good, since it "escapes" into the imperative programming model. Not sure if handle is a good fit here, since that might be more specific to how React attaches that to a passed ref.
But I can't think of anything better a.t.m., so registerImperativeHandle is currently the best option.
Implementation details
-
In React, the object you pass to
useImperativeHandleis a function. Most likely this is because the render function is executed multiple times, and you don't want to re-create these objects every time. – In Muban, the setup gets only executed once, and individually for each component, so passing the object directly is totally fine. -
We must think of scenarios where the
refitself is lazy / optional, and the component might not exist yet. But since the imperative handle is attached to the component, as soon as the component exists, the handle should also exist. However, for lazy/optional components, the timing of where we assign the component to the internalref(which could trigger awatch) should ideally be after the setup of the new child component is called. -
We need to make sure that
typesare properly resolved.defineComponentalready has some generics, so they need to "shift" to make room for this one one, and also probably should get default values, since you cannot just pass 1 of the non-optional ones – you would have to pass all of them.
@psimk @jspolancor @larsvanbraam @ThijsTyZ any thoughts, preferences, opinions?