Skip to content
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

Add forwardRef wrapper to clean up component types #19812

Closed
wants to merge 7 commits into from

Conversation

behowell
Copy link
Contributor

Pull request checklist

  • Include a change request file using $ yarn change

Description of changes

This is a follow-up from PR #19793. It adds a wrapper for forwardRef for use by our components. This has the following benefits:

  • Automatically infers the ref type from the component's props if they were created using IntrinsicShorthandProps. This reduces the chance of human error/laziness in getting the wrong type for the ref prop.
  • Cleans up the types in the .api.md file by avoiding using React.PropsWithoutRef, which React.forwardRef uses. This type caused intrinsic element types to be exploded inside the .api.md file (listing every HTML attribute individually).
  • Standardizes the type of our components to be React.FunctionComponent<ComponentProps> instead of the current mix of React.ForwardRefExoticComponent, React.FunctionComponent, etc.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Sep 15, 2021

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 31f0994:

Sandbox Source
Fluent UI React Starter Configuration

@fabricteam
Copy link
Collaborator

fabricteam commented Sep 15, 2021

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-accordion
Accordion (including children components)
55.178 kB
17.339 kB
55.213 kB
17.371 kB
-35 B
-32 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
164.096 kB
46.675 kB
164.196 kB
46.769 kB
-100 B
-94 B
react-components
react-components: FluentProvider & webLightTheme
35.758 kB
11.378 kB
35.754 kB
11.385 kB
4 B
-7 B
react-image
Image
9.812 kB
3.954 kB
9.808 kB
3.948 kB
4 B
6 B
react-input
Input
31.644 kB
11.344 kB
31.64 kB
11.328 kB
4 B
16 B
react-link
Link
13.726 kB
5.67 kB
13.722 kB
5.665 kB
4 B
5 B
react-menu
Menu (including children components)
103.374 kB
31.421 kB
103.388 kB
31.418 kB
-14 B
3 B
react-menu
Menu (including selectable components)
105.61 kB
31.773 kB
105.644 kB
31.771 kB
-34 B
2 B
react-popover
Popover
100.419 kB
30.075 kB
100.415 kB
30.075 kB
4 B
react-provider
FluentProvider
15.756 kB
5.751 kB
15.752 kB
5.762 kB
4 B
-11 B
react-slider
Slider
30.705 kB
9.74 kB
30.701 kB
9.745 kB
4 B
-5 B
react-switch
Switch
18.824 kB
6.269 kB
18.82 kB
6.273 kB
4 B
-4 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
56.558 kB
15.66 kB
react-badge
Badge
24.343 kB
7.165 kB
react-badge
CounterBadge
27.156 kB
7.851 kB
react-badge
PresenseBadge
237 B
177 B
react-button
Button
22.932 kB
6.984 kB
react-button
CompoundButton
28.215 kB
7.834 kB
react-button
MenuButton
24.733 kB
7.546 kB
react-button
ToggleButton
32.527 kB
7.601 kB
react-divider
Divider
15.889 kB
5.747 kB
react-label
Label
9.397 kB
3.839 kB
react-make-styles
makeStaticStyles (runtime)
7.59 kB
3.321 kB
react-make-styles
makeStyles + mergeClasses (runtime)
22.135 kB
8.356 kB
react-make-styles
makeStyles + mergeClasses (build time)
2.557 kB
1.202 kB
react-portal
Portal
6.725 kB
2.237 kB
react-positioning
usePopper
23.145 kB
7.942 kB
react-text
Text - Default
11.798 kB
4.452 kB
react-text
Text - Wrappers
15.414 kB
4.734 kB
react-tooltip
Tooltip
46.016 kB
15.656 kB
react-utilities
SSRProvider
213 B
170 B
🤖 This report was generated against 5107c383813b6ca883958375d714de18ead75e06

@size-auditor
Copy link

size-auditor bot commented Sep 15, 2021

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 5107c383813b6ca883958375d714de18ead75e06 (build)

@fabricteam
Copy link
Collaborator

fabricteam commented Sep 15, 2021

Perf Analysis (@fluentui/react)

Scenario Render type Master Ticks PR Ticks Iterations Status
Panel mount 2188 1299 1000 Possible regression
All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 884 846 5000
BaseButton mount 844 838 5000
Breadcrumb mount 2480 2473 1000
ButtonNext mount 419 418 5000
Checkbox mount 1420 1410 5000
CheckboxBase mount 1192 1155 5000
ChoiceGroup mount 4418 4400 5000
ComboBox mount 943 897 1000
CommandBar mount 9700 9601 1000
ContextualMenu mount 5987 6146 1000
DefaultButton mount 1086 1043 5000
DetailsRow mount 3495 3493 5000
DetailsRowFast mount 3548 3455 5000
DetailsRowNoStyles mount 3297 3302 5000
Dialog mount 2319 2326 1000
DocumentCardTitle mount 135 127 1000
Dropdown mount 3069 3026 5000
FluentProviderNext mount 7068 7128 5000
FluentProviderWithTheme mount 327 327 10
FluentProviderWithTheme virtual-rerender 88 92 10
FluentProviderWithTheme virtual-rerender-with-unmount 438 445 10
FocusTrapZone mount 1658 1668 5000
FocusZone mount 1739 1734 5000
IconButton mount 1633 1639 5000
Label mount 318 314 5000
Layer mount 2827 2814 5000
Link mount 435 442 5000
MakeStyles mount 1734 1766 50000
MenuButton mount 1378 1362 5000
MessageBar mount 1897 1909 5000
Nav mount 3049 3058 1000
OverflowSet mount 1038 1071 5000
Panel mount 2188 1299 1000 Possible regression
Persona mount 800 777 1000
Pivot mount 1356 1347 1000
PrimaryButton mount 1230 1207 5000
Rating mount 7183 7189 5000
SearchBox mount 1235 1216 5000
Shimmer mount 2372 2403 5000
Slider mount 1821 1855 5000
SpinButton mount 4680 4748 5000
Spinner mount 404 383 5000
SplitButton mount 2960 2970 5000
Stack mount 471 473 5000
StackWithIntrinsicChildren mount 1465 1464 5000
StackWithTextChildren mount 4244 4292 5000
SwatchColorPicker mount 9640 9718 5000
Tabs mount 1334 1326 1000
TagPicker mount 2432 2499 5000
TeachingBubble mount 12575 12494 5000
Text mount 398 390 5000
TextField mount 1309 1277 5000
ThemeProvider mount 1122 1141 5000
ThemeProvider virtual-rerender 572 570 5000
ThemeProvider virtual-rerender-with-unmount 1769 1775 5000
Toggle mount 738 761 5000
buttonNative mount 110 114 5000

Perf Analysis (@fluentui/react-northstar)

Perf tests with no regressions
Scenario Current PR Ticks Baseline Ticks Ratio
ChatDuplicateMessagesPerf.default 277 254 1.09:1
IconMinimalPerf.default 614 562 1.09:1
AvatarMinimalPerf.default 187 177 1.06:1
TextAreaMinimalPerf.default 484 458 1.06:1
GridMinimalPerf.default 319 304 1.05:1
AttachmentMinimalPerf.default 145 139 1.04:1
CarouselMinimalPerf.default 431 416 1.04:1
ImageMinimalPerf.default 360 345 1.04:1
LabelMinimalPerf.default 363 350 1.04:1
PopupMinimalPerf.default 568 545 1.04:1
PortalMinimalPerf.default 173 167 1.04:1
CardMinimalPerf.default 520 505 1.03:1
ChatMinimalPerf.default 617 597 1.03:1
DropdownManyItemsPerf.default 629 613 1.03:1
FlexMinimalPerf.default 274 266 1.03:1
SegmentMinimalPerf.default 325 316 1.03:1
SkeletonMinimalPerf.default 330 319 1.03:1
AnimationMinimalPerf.default 390 381 1.02:1
FormMinimalPerf.default 379 370 1.02:1
HeaderMinimalPerf.default 341 334 1.02:1
RadioGroupMinimalPerf.default 412 405 1.02:1
SliderMinimalPerf.default 1509 1475 1.02:1
ToolbarMinimalPerf.default 887 870 1.02:1
VideoMinimalPerf.default 579 568 1.02:1
AttachmentSlotsPerf.default 1013 1000 1.01:1
ButtonSlotsPerf.default 520 513 1.01:1
DialogMinimalPerf.default 721 715 1.01:1
DropdownMinimalPerf.default 2966 2941 1.01:1
ItemLayoutMinimalPerf.default 1124 1108 1.01:1
ListWith60ListItems.default 593 588 1.01:1
LoaderMinimalPerf.default 651 643 1.01:1
MenuButtonMinimalPerf.default 1529 1517 1.01:1
ProviderMergeThemesPerf.default 1567 1549 1.01:1
ReactionMinimalPerf.default 348 343 1.01:1
TableManyItemsPerf.default 1785 1771 1.01:1
TextMinimalPerf.default 330 327 1.01:1
TooltipMinimalPerf.default 955 946 1.01:1
AccordionMinimalPerf.default 139 139 1:1
CheckboxMinimalPerf.default 2533 2537 1:1
EmbedMinimalPerf.default 3886 3897 1:1
LayoutMinimalPerf.default 336 337 1:1
ListCommonPerf.default 570 571 1:1
MenuMinimalPerf.default 776 776 1:1
RosterPerf.default 1093 1095 1:1
SplitButtonMinimalPerf.default 3871 3867 1:1
StatusMinimalPerf.default 643 640 1:1
TableMinimalPerf.default 373 374 1:1
CustomToolbarPrototype.default 3710 3700 1:1
TreeMinimalPerf.default 729 730 1:1
BoxMinimalPerf.default 312 316 0.99:1
ButtonOverridesMissPerf.default 1606 1617 0.99:1
DatepickerMinimalPerf.default 5115 5157 0.99:1
ListNestedPerf.default 498 503 0.99:1
ProviderMinimalPerf.default 939 950 0.99:1
TreeWith60ListItems.default 167 169 0.99:1
AlertMinimalPerf.default 249 254 0.98:1
DividerMinimalPerf.default 320 326 0.98:1
InputMinimalPerf.default 1174 1192 0.98:1
HeaderSlotsPerf.default 694 714 0.97:1
ListMinimalPerf.default 468 481 0.97:1
ButtonMinimalPerf.default 147 153 0.96:1
ChatWithPopoverPerf.default 327 339 0.96:1
RefMinimalPerf.default 212 222 0.95:1

Copy link
Member

@khmakoto khmakoto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have qualms with this since we are still using React.forwardRef underneath, although we might want to call it out in more front-facing documentation if we do adopt this.

* const Example = forwardRef<ExampleProps & React.RefAttributes<HTMLElement>>((props, ref) => { ... });
* ```
*
* @returns the same as `React.forwardRef`, re-typed to `React.FunctionCompoonent<Props>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
* @returns the same as `React.forwardRef`, re-typed to `React.FunctionCompoonent<Props>`.
* @returns the same as `React.forwardRef`, re-typed to `React.FunctionComponent<Props>`.

@@ -70,6 +70,11 @@ export type IntrinsicShorthandProps<
*/
export type IsSingleton<T extends string> = { [K in T]: Exclude<T, K> extends never ? true : false }[T];

/**
* Extends a props object with a LegagyRef, to be a normal Ref
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
* Extends a props object with a LegagyRef, to be a normal Ref
* Extends a props object with a LegacyRef, to be a normal Ref

* Expects the Props type to already have a `ref` attribute (which is the case if using `IntrinsicShorthandProps`).
* If not, add the ref attribute using `React.RefAttributes`:
* ```
* const Example = forwardRef<ExampleProps & React.RefAttributes<HTMLElement>>((props, ref) => { ... });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to encourage callers to have ExampleProps be intersected with the ref attributes rather than doing it within the forwardRef call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure I understand, but is the suggestion that this function should add a ref attribute if it doesn't exist?

If the Props param doesn't have a ref attribute, this function can't add it because it doesn't know what type the ref should be: Ref<HTMLDivElement> or Ref<HTMLSpanElement>, etc. (It's not correct to just use Ref<HTMLElement>, since that would allow you to assign e.g. Ref<HTMLButtonElement> to a component whose root type is <input>.)

The Props param to this function is required to have a ref prop, and this example was demonstrating how to add it if it's not already there.

ecraig12345
ecraig12345 previously approved these changes Sep 16, 2021
@ling1726
Copy link
Member

ling1726 commented Sep 16, 2021

Are we sure that we want to use FC as the type. When you call forwardRef or createContext you create objects and not actually a 'normal' function component. The return of forwardRef is not a functional component, and actually can't be used as such. Using them in JSX is similar... but they are definitely not the same thing

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/927f70dd89119f11a47688ed5eb4e0e082bef219/types/react/index.d.ts#L355

@ecraig12345 ecraig12345 dismissed their stale review September 17, 2021 01:06

more discussion

@ecraig12345
Copy link
Member

Are we sure that we want to use FC as the type. When you call forwardRef or createContext you create objects and not actually a 'normal' function component. The return of forwardRef is not a functional component, and actually can't be used as such. Using them in JSX is similar... but they are definitely not the same thing

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/927f70dd89119f11a47688ed5eb4e0e082bef219/types/react/index.d.ts#L355

Interesting, didn't realize that. (This link is to 16 but it's similar in latest.)
https://github.com/facebook/react/blob/v16.14.0/packages/react/src/ReactForwardRef.js#L46-L66

I guess we could either have the wrapper return React.ForwardRefExoticComponent<Props>, or just declare that type inline for the individual components?

@ling1726
Copy link
Member

I guess we could either have the wrapper return React.ForwardRefExoticComponent<Props>, or just declare that type inline for the individual components?

I don't think anything is wrong with using the default types for ForwardRef matching the wrong HTMLElement type should still be caught by type checking

@ecraig12345
Copy link
Member

I guess we could either have the wrapper return React.ForwardRefExoticComponent<Props>, or just declare that type inline for the individual components?

I don't think anything is wrong with using the default types for ForwardRef matching the wrong HTMLElement type should still be caught by type checking

Using the return value without an explicit type results in unhelpful expansion of types. It's not the end of the world, but I'd still strongly prefer to have an explicit type of some kind.

@behowell
Copy link
Contributor Author

@ling1726

I don't think anything is wrong with using the default types for ForwardRef matching the wrong HTMLElement type should still be caught by type checking

The main problem is that fowardRef's default types internally use PropsWithoutRef, which is what explodes the Props type, and it's unnecessary since our types already have the correct ref prop. I could return ForwardRefExoticComponent<Props> instead, which I think would be fine.

@ecraig12345
Copy link
Member

I don't think anything is wrong with using the default types for ForwardRef matching the wrong HTMLElement type should still be caught by type checking

The main problem is that fowardRef's default types internally use PropsWithoutRef, which is what explodes the Props type, and it's unnecessary since our types already have the correct ref prop. I could return ForwardRefExoticComponent<Props> instead, which I think would be fine.

I just started trying that out locally and it breaks down for Link. 😢 Best thing I can come up with is a cast.

export const Link: React.ForwardRefExoticComponent<LinkProps> = React.forwardRef((props, ref) => {
  const state = useLink(props, ref as React.Ref<HTMLAnchorElement | HTMLButtonElement>);

@ecraig12345
Copy link
Member

Hmm actually it breaks down in the same way in other places, I just didn't see it at first due to outdated types cached in VS Code's memory or something.

export const Accordion: React.ForwardRefExoticComponent<AccordionProps> = React.forwardRef((props, ref) => {
  const state = useAccordion(props, ref); // line 11
ERR! [2:06:34 PM] x src/components/Accordion/Accordion.tsx(11,37): error TS2345: Argument of type '((instance: unknown) => void) | MutableRefObject<unknown> | null' is not assignable to parameter of type 'Ref<HTMLElement>'.
ERR!   Type 'MutableRefObject<unknown>' is not assignable to type 'Ref<HTMLElement>'.
ERR!     Type 'MutableRefObject<unknown>' is not assignable to type 'RefObject<HTMLElement>'.
ERR!       Types of property 'current' are incompatible.
ERR!         Type 'unknown' is not assignable to type 'HTMLElement | null'.
ERR!           Type 'unknown' is not assignable to type 'HTMLElement'.
ERR! src/components/AccordionHeader/AccordionHeader.tsx(12,43): error TS2345: Argument of type '((instance: unknown) => void) | MutableRefObject<unknown> | null' is not assignable to parameter of type 'Ref<HTMLElement>'.
ERR!   Type 'MutableRefObject<unknown>' is not assignable to type 'Ref<HTMLElement>'.
ERR!     Type 'MutableRefObject<unknown>' is not assignable to type 'RefObject<HTMLElement>'.
ERR! src/components/AccordionItem/AccordionItem.tsx(11,41): error TS2345: Argument of type '((instance: unknown) => void) | MutableRefObject<unknown> | null' is not assignable to parameter of type 'Ref<HTMLElement>'.
ERR!   Type 'MutableRefObject<unknown>' is not assignable to type 'Ref<HTMLElement>'.
ERR!     Type 'MutableRefObject<unknown>' is not assignable to type 'RefObject<HTMLElement>'.
ERR! src/components/AccordionPanel/AccordionPanel.tsx(11,42): error TS2345: Argument of type '((instance: unknown) => void) | MutableRefObject<unknown> | null' is not assignable to parameter of type 'Ref<HTMLElement>'.
ERR!   Type 'MutableRefObject<unknown>' is not assignable to type 'Ref<HTMLElement>'.
ERR!     Type 'MutableRefObject<unknown>' is not assignable to type 'RefObject<HTMLElement>'.

@layershifter
Copy link
Member

Please do not merge this till Monday, I will leave more detailed comment. TLDR; this function will not be annotated with PURE comment ➡️ cause problems with tree shaking.

@behowell
Copy link
Contributor Author

@layershifter

Please do not merge this till Monday, I will leave more detailed comment. TLDR; this function will not be annotated with PURE comment ➡️ cause problems with tree shaking.

👍🏻 I'll wait. I did just change it to be simply re-exporting forwardRef with a different type; I'm not sure if that changes the issue you're referring to (I don't fully understand the problem).

@ecraig12345
Copy link
Member

@layershifter

Please do not merge this till Monday, I will leave more detailed comment. TLDR; this function will not be annotated with PURE comment ➡️ cause problems with tree shaking.

👍🏻 I'll wait. I did just change it to be simply re-exporting forwardRef with a different type; I'm not sure if that changes the issue you're referring to (I don't fully understand the problem).

Rather than re-exporting with a different type, could we just add a helper type and use that in the declarations? like const Accordion: CustomForwardRefComponent<whatever> = ...

@layershifter
Copy link
Member

layershifter commented Sep 20, 2021

Tested on webpack@5.21.2 & terser@5.5.1.

Background

Webpack does not do dead-code elimination, it does only module concatenation and it's enabled by default 🎉

Minification and dead code elimination is done by Terser. It's important to have concatenated modules as then Terser operates in a flattened scope.

Check examples below 👇

Sample input

// exports.js
export const foo = "foo";
export const bar = "bar";
// main.js
import { foo, bar } from "./exports";
console.log(foo);
Webpack's output with `optimization: { concatenateModules: false }`
/******/ (() => {
  // webpackBootstrap
  /******/ "use strict";
  /******/ var _a__WEBPACK_IMPORTED_MODULE_0__,
    __webpack_modules__ = {
      /***/ 85: /***/ (
        __unused_webpack_module,
        __webpack_exports__,
        __webpack_require__
      ) => {
        /* harmony export */ __webpack_require__.d(__webpack_exports__, {
          /* harmony export */ R: () => /* binding */ foo
          /* harmony export */
        });
        /* unused harmony export bar */
        const foo = "foo";
      }
      /***/
      /******/
    },
    __webpack_module_cache__ = {}; /******/ /******/ // The module cache // The require function
  /************************************************************************/
  /******/ /******/ /******/ function __webpack_require__(moduleId) {
    /******/ // Check if module is in cache
    /******/ if (__webpack_module_cache__[moduleId])
      /******/ return __webpack_module_cache__[moduleId].exports; // Create a new module (and put it into the cache)
    /******/
    /******/ /******/ var module = (__webpack_module_cache__[moduleId] = {
      /******/ // no module.id needed
      /******/ // no module.loaded needed
      /******/ exports: {}
      /******/
    }); /******/ /******/ // Execute the module function // Return the exports of the module
    /******/
    /******/ /******/ /******/ return (
      __webpack_modules__[moduleId](
        module,
        module.exports,
        __webpack_require__
      ),
      module.exports
    );
    /******/
  } /******/ /* webpack/runtime/define property getters */ // define getter functions for harmony exports
  /******/
  /************************************************************************/
  /******/ /******/ /******/ (__webpack_require__.d = (exports, definition) => {
    /******/ /******/ for (var key in definition)
      __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key) &&
        /******/ Object.defineProperty(exports, key, {
          enumerable: !0,
          get: definition[key]
        });
    /******/
    /******/
  }),
    /******/ (__webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(
        obj,
        prop
      )) /************************************************************************/ /******/,
    /******/ (_a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(85)),
    console.log(_a__WEBPACK_IMPORTED_MODULE_0__ /* .foo */.R);
})();
/******/

Webpack's output with enabled module concatenation

/******/ (() => {
  // webpackBootstrap
  /******/ "use strict";
  // CONCATENATED MODULE: ./src/main.js
  console.log("foo");
})();
/******/

Why do we care at all about module concatenation?

React is CommonJS module and cannot use module concatenation as it works only for ESM 🙃

  ./node_modules/react/index.js 190 bytes [built] [code generated]
    Statement (ExpressionStatement) with side effects in source code at 4:2-60
    ModuleConcatenation bailout: Module is not an ECMAScript module

So after using React's API we will get something different:

// exports.js
+import * as React from "react";

-export const foo = "foo";
-export const bar = "bar";
+export const foo = React.forwardRef("foo");
+export const bar = React.forwardRef("bar");

Output from Webpack:

/************************************************************************/ (() => {
  // EXTERNAL MODULE: ./node_modules/react/index.js
  var react = __webpack_require__(294);
  // CONCATENATED MODULE: ./src/exports.js
  const foo = react.forwardRef("foo");
  react.forwardRef("bar");
  // CONCATENATED MODULE: ./src/main.js
  console.log(foo);
})();

⚠ You could notice that react.forwardRef("bar"); stays in the bundle even if it is not used. The problem is that instead of "bar" we will get the whole component's code with all dependencies.

The output will be the same even if we use imports from separate files (importing a module is enough to create issues):

// ⚠ exports.js was split to two files
import { foo } from "./foo";
import { bar } from "./bar";

console.log(foo);

In real life this could be conditional import based on env variables or a flag.

This happens because React.forwardRef is considered as a side-effect by Terser and cannot be removed. If a file exports multiple entries, for example, a component and some consts, a side-effect will still stay in the bundle (#14007).

This could be solved by /*#__PURE__*/ annotation (babel/babel#11428):

// bar.js
import * as React from "react";

-export const bar = React.forwardRef("bar");
+export const bar = /*#__PURE__*/ React.forwardRef("bar");

⬇⬇⬇

/************************************************************************/ (() => {
  // CONCATENATED MODULE: ./src/foo.js
  const foo = __webpack_require__(294).forwardRef("foo");
  // CONCATENATED MODULE: ./src/main.js
  console.log(foo);
})();

That's why we have this Babel plugin in our pipeline for vNext components, #18037.

How this PR can cause problems?

Babel plugin annotates only calls of React.forwardRef(), it will ignore our wrapper:

// ref.js
import * as React from "react";

export const forwardRef = fn => {
  return /*#__PURE__*/ React.forwardRef(fn);
};
// EXTERNAL MODULE: ./node_modules/react/index.js
var react = __webpack_require__(294);
// CONCATENATED MODULE: ./src/ref.js
const forwardRef = fn => /*#__PURE__*/ react.forwardRef(fn),
  foo = forwardRef("foo");
forwardRef("bar");
// CONCATENATED MODULE: ./src/main.js
console.log(foo);

👆 bar is again there 😥

The issue is still present for the latest change in this PR (a707aa9).

// ref.js
import * as React from "react";
export const forwardRef = /*#__PURE__*/ React.forwardRef;
/******/ (() => {
  // webpackBootstrap
  /******/ "use strict";
  // CONCATENATED MODULE: external "react"
  const forwardRef = /*#__PURE__*/ react.forwardRef,
    foo = forwardRef("foo");
  forwardRef("bar");
  // CONCATENATED MODULE: ./src/main.js
  console.log(foo);
})();
/******/

Is it solvable?

Yes ✅ We just need to use /*#__PURE__*/ annotation:

// foo.js
import { forwardRef } from "./ref";

-export const foo = forwardRef("foo");
+export const foo = /*#__PURE__*/ forwardRef("foo");

Then output becomes great again:

/******/ (() => {
  // webpackBootstrap
  /******/ "use strict";
  // CONCATENATED MODULE: external "react"
  const ref_forwardRef = react.forwardRef;
  // CONCATENATED MODULE: ./src/main.js
  console.log(/*#__PURE__*/ ref_forwardRef("foo"));
})();
/******/

💡 We can still do it in automated way via babel-plugin-annotate-pure-imports that I made some time ago (#14007):

.babelrc.json

{
  "plugins": [
    "babel-plugin-annotate-pure-imports",
    {
      "imports": {
        "@fluentui/react-utilities": "forwardRef"
      }
    }
  ]
}

However, taking the complexity into account it might not worth to do this change to satisfy only API extractor.

@ecraig12345
Copy link
Member

@layershifter Thanks for the explanation!

However, taking the complexity into account it might not worth to do this change to satisfy only API extractor.

I would add that this isn't necessarily just API Extractor. It's at least two different reasons:

  • If someone hovers over a type or does "go to definition" in our .d.ts files, it's helpful if that shows the actual props type and not some expanded garbage
  • The point of the *.api.md files is to ensure that API changes are obvious and make them easier to review. Having exploded types detracts from that.

I had been looking at this some on Friday, so especially since Ben is out now (sooner than expected), I may make another PR with a similar goal but a slightly different approach.

@varholak-peter
Copy link
Contributor

Is this superseded by #19923? If yes, can this be closed?

@layershifter
Copy link
Member

Is this superseded by #19923? If yes, can this be closed?

Yes, thanks 👍

@behowell behowell deleted the slotprops-forwardref branch July 25, 2022 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants