Components: Add contextConnectWithoutRef() to bypass ref forwarding#43611
Components: Add contextConnectWithoutRef() to bypass ref forwarding#43611
Conversation
|
Size Change: -1 B (0%) Total Size: 1.24 MB
ℹ️ View Unchanged
|
There was a problem hiding this comment.
It would be cool if we could automatically detect the presence of a ref argument without having to manually set the noForwardRef option. However, I think it's impossible because we cannot reliably detect in JS whether a component function takes one argument (props) or two (props & ref). If we can't detect in JS, we can't do this conditional.
The next best thing would be to detect in TS whether the noForwardRef boolean is wrong, given the signature of the component function. I tried a bunch of ways with generics and conditional types, but unfortunately could not get it to work! It's a nice-to-have if anyone wants to try figuring it out.
There was a problem hiding this comment.
Yay, I found out a better way — TypeScript will now throw an error if the ref stuff is wrong. I updated the PR description.
9686cea to
7ebd4f5
Compare
| type AcceptsTwoArgs< | ||
| F extends ( ...args: any ) => any, | ||
| ErrorMessage = never | ||
| > = Parameters< F >[ 'length' ] extends 2 ? {} : ErrorMessage; |
There was a problem hiding this comment.
Turns out, TypeScript is (justifiably) loose about function arity. No wonder my previous attempts didn't work!
Here is a minimal repro in a TS Playground if you're interested in what's going on here and why it's necessary. (Solution courtesy of StackOverflow 😌)
ciampo
left a comment
There was a problem hiding this comment.
Great work here! This is not an easy task, and your changes definitely feel like an improvement
Previously, passing a ref-less function to contextConnect would cause dev mode React to throw a warning at runtime. This could be hard to notice if there are no render tests for the component.
Interesting! Were there any examples of such components?
| ): WordPressComponentFromProps< Parameters< C >[ 0 ] > { | ||
| const WrappedComponent = options?.forwardsRef | ||
| ? forwardRef< any, Parameters< C >[ 0 ] >( Component ) | ||
| : Component; |
There was a problem hiding this comment.
I wonder if we could simplify the code here by:
- using
ComponentProps< C >(or similar) instead of the more implicitParameters< C >[0] - assigning that type directly to
WrappedComponent— which would remove the need to specify an explicit function return type, and could potentially help with those@ts-expect-errorcomments later in the code
There was a problem hiding this comment.
Sounded hopeful but I couldn't make it happen 🙁 The blocker being, ComponentProps< C > needs C to be a JSXElementConstructor, while forwardRef() needs C to be a ForwardRefRenderFunction, and the two are not compatible 😵
There was a problem hiding this comment.
🤯
I tried to use some infer magic and I came up with this
diff --git a/packages/components/src/ui/context/context-connect.ts b/packages/components/src/ui/context/context-connect.ts
index 0214ecc686..3a17897483 100644
--- a/packages/components/src/ui/context/context-connect.ts
+++ b/packages/components/src/ui/context/context-connect.ts
@@ -64,15 +64,21 @@ export function contextConnectWithoutRef< P >(
// This is an (experimental) evolution of the initial connect() HOC.
// The hope is that we can improve render performance by removing functional
// component wrappers.
+type InferredComponentProps< C > = C extends (
+ props: infer P,
+ ref: ForwardedRef< any >
+) => JSX.Element | null
+ ? P
+ : never;
function _contextConnect<
C extends ( props: any, ref: ForwardedRef< any > ) => JSX.Element | null
>(
Component: C,
namespace: string,
options?: ContextConnectOptions
-): WordPressComponentFromProps< Parameters< C >[ 0 ] > {
+): WordPressComponentFromProps< InferredComponentProps< C > > {
const WrappedComponent = options?.forwardsRef
- ? forwardRef< any, Parameters< C >[ 0 ] >( Component )
+ ? forwardRef< any, InferredComponentProps< C > >( Component )
: Component;
if ( typeof namespace === 'undefined' ) {But I'm not sure if it's a better solution than the current one — plus, should we wrap these props in ComponentPropsWithoutRef and/or ComponentPropsWithRef ?
It's quite entangled!
There was a problem hiding this comment.
Yeah, maybe we can add that utility type if we start to encounter it in more places.
7ebd4f5 to
4757bba
Compare
4757bba to
3539ef4
Compare
Fortunately I couldn't find any 😌 |
681b1b0 to
57e84cb
Compare
|
Rebased and ready for final review 🙏 |
| ): WordPressComponentFromProps< Parameters< C >[ 0 ] > { | ||
| const WrappedComponent = options?.forwardsRef | ||
| ? forwardRef< any, Parameters< C >[ 0 ] >( Component ) | ||
| : Component; |
There was a problem hiding this comment.
🤯
I tried to use some infer magic and I came up with this
diff --git a/packages/components/src/ui/context/context-connect.ts b/packages/components/src/ui/context/context-connect.ts
index 0214ecc686..3a17897483 100644
--- a/packages/components/src/ui/context/context-connect.ts
+++ b/packages/components/src/ui/context/context-connect.ts
@@ -64,15 +64,21 @@ export function contextConnectWithoutRef< P >(
// This is an (experimental) evolution of the initial connect() HOC.
// The hope is that we can improve render performance by removing functional
// component wrappers.
+type InferredComponentProps< C > = C extends (
+ props: infer P,
+ ref: ForwardedRef< any >
+) => JSX.Element | null
+ ? P
+ : never;
function _contextConnect<
C extends ( props: any, ref: ForwardedRef< any > ) => JSX.Element | null
>(
Component: C,
namespace: string,
options?: ContextConnectOptions
-): WordPressComponentFromProps< Parameters< C >[ 0 ] > {
+): WordPressComponentFromProps< InferredComponentProps< C > > {
const WrappedComponent = options?.forwardsRef
- ? forwardRef< any, Parameters< C >[ 0 ] >( Component )
+ ? forwardRef< any, InferredComponentProps< C > >( Component )
: Component;
if ( typeof namespace === 'undefined' ) {But I'm not sure if it's a better solution than the current one — plus, should we wrap these props in ComponentPropsWithoutRef and/or ComponentPropsWithRef ?
It's quite entangled!
What?
Adds a separate
contextConnectWithoutRef()function so components can bypass theforwardRef()wrapping.Why?
Not all components need to forward refs.
How?
Moves the existing
contextConnectfunction to a private internal function, and instead exposes two separate functionscontextConnectandcontextConnectWithoutRef. TypeScript will now error when a ref-less function is passed tocontextConnect, and will surface a helpful warning message.Previously, passing a ref-less function to
contextConnectwould cause dev mode React to throw a warning at runtime. This could be hard to notice if there are no render tests for the component.Testing Instructions
✅ Types build without error