-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Components: Add contextConnectWithoutRef() to bypass ref forwarding #43611
Conversation
Size Change: -1 B (0%) Total Size: 1.24 MB
ℹ️ View Unchanged
|
const { memo: memoProp = false } = options; | ||
const { memo: memoProp = false, noForwardRef } = options; | ||
|
||
let WrappedComponent = noForwardRef ? Component : forwardRef( Component ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 😌)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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-error
comments later in the code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤯
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 🙏 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
): WordPressComponentFromProps< Parameters< C >[ 0 ] > { | ||
const WrappedComponent = options?.forwardsRef | ||
? forwardRef< any, Parameters< C >[ 0 ] >( Component ) | ||
: Component; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤯
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
contextConnect
function to a private internal function, and instead exposes two separate functionscontextConnect
andcontextConnectWithoutRef
. 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
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.Testing Instructions
✅ Types build without error