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

RFC: standardize event handlers arguments #18974

Merged
merged 7 commits into from
Aug 9, 2021
Merged

Conversation

layershifter
Copy link
Member

@layershifter layershifter commented Jul 16, 2021

All credit there goes to @dzearing, this RFC was originally published in #12588.


We should provide guidance on how events handlers (onChange, onOpen, etc.) are exposed across components. Today we have a discrepancy between @fluentui/react-northstar (Fluent UI Northstar) , @fluentui/react (Fluent UI v8) and converged components.


🧾 Rendered preview

@layershifter layershifter added the Type: RFC Request for Feedback label Jul 16, 2021
@layershifter layershifter requested review from a team July 16, 2021 12:01
@layershifter layershifter marked this pull request as ready for review July 16, 2021 12:01
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jul 16, 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 cf231cd:

Sandbox Source
Fluent UI React Starter Configuration

@size-auditor
Copy link

size-auditor bot commented Jul 16, 2021

Asset size changes

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

Baseline commit: 57a2bc1f8a6ec7859bf4a3509696681d39602561 (build)

@fabricteam
Copy link
Collaborator

fabricteam commented Jul 16, 2021

📊 Bundle size report

Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-accordion
Accordion (including children components)
75.578 kB
22.287 kB
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
24.934 kB
8.001 kB
react-button
CompoundButton
30.226 kB
8.878 kB
react-button
MenuButton
26.521 kB
8.509 kB
react-button
ToggleButton
34.531 kB
8.637 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
176.955 kB
50.083 kB
react-components
react-components: FluentProvider & webLightTheme
36.237 kB
11.596 kB
react-divider
Divider
15.889 kB
5.747 kB
react-image
Image
10.642 kB
4.264 kB
react-label
Label
9.397 kB
3.839 kB
react-link
Link
14.715 kB
6.012 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-menu
Menu (including children components)
114.607 kB
34.555 kB
react-menu
Menu (including selectable components)
116.707 kB
34.825 kB
react-popover
Popover
124.178 kB
36.122 kB
react-portal
Portal
7.78 kB
2.672 kB
react-positioning
usePopper
23.154 kB
7.942 kB
react-provider
FluentProvider
16.235 kB
5.972 kB
react-theme
Teams: all themes
32.941 kB
6.674 kB
react-theme
Teams: Light theme
20.247 kB
5.662 kB
react-tooltip
Tooltip
45.278 kB
15.451 kB
react-utilities
SSRProvider
213 B
170 B
🤖 This report was generated against 57a2bc1f8a6ec7859bf4a3509696681d39602561

@fabricteam
Copy link
Collaborator

fabricteam commented Jul 16, 2021

Perf Analysis (@fluentui/react)

No significant results to display.

All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 845 843 5000
BaseButton mount 830 859 5000
Breadcrumb mount 2529 2464 1000
ButtonNext mount 393 401 5000
Checkbox mount 1407 1427 5000
CheckboxBase mount 1207 1200 5000
ChoiceGroup mount 4463 4444 5000
ComboBox mount 932 914 1000
CommandBar mount 9586 9603 1000
ContextualMenu mount 5929 5916 1000
DefaultButton mount 1061 1073 5000
DetailsRow mount 3497 3543 5000
DetailsRowFast mount 3566 3464 5000
DetailsRowNoStyles mount 3310 3340 5000
Dialog mount 2040 2057 1000
DocumentCardTitle mount 130 132 1000
Dropdown mount 3042 3029 5000
FluentProviderNext mount 7104 7127 5000
FocusTrapZone mount 1712 1716 5000
FocusZone mount 1685 1674 5000
IconButton mount 1581 1638 5000
Label mount 329 321 5000
Layer mount 1698 1658 5000
Link mount 439 431 5000
MakeStyles mount 1730 1728 50000
MenuButton mount 1385 1387 5000
MessageBar mount 1892 1898 5000
Nav mount 3085 3060 1000
OverflowSet mount 1005 983 5000
Panel mount 2023 1973 1000
Persona mount 789 787 1000
Pivot mount 1321 1326 1000
PrimaryButton mount 1193 1209 5000
Rating mount 7218 7200 5000
SearchBox mount 1243 1229 5000
Shimmer mount 2402 2395 5000
Slider mount 1854 1832 5000
SpinButton mount 4675 4729 5000
Spinner mount 403 402 5000
SplitButton mount 2949 2984 5000
Stack mount 481 468 5000
StackWithIntrinsicChildren mount 1448 1474 5000
StackWithTextChildren mount 4241 4264 5000
SwatchColorPicker mount 9686 9605 5000
Tabs mount 1339 1330 1000
TagPicker mount 2453 2472 5000
TeachingBubble mount 11224 11289 5000
Text mount 383 393 5000
TextField mount 1285 1298 5000
ThemeProvider mount 1130 1127 5000
ThemeProvider virtual-rerender 564 572 5000
Toggle mount 756 767 5000
buttonNative mount 104 114 5000

Perf Analysis (@fluentui/react-northstar)

Perf tests with no regressions
Scenario Current PR Ticks Baseline Ticks Ratio
AccordionMinimalPerf.default 149 136 1.1:1
ChatWithPopoverPerf.default 351 326 1.08:1
BoxMinimalPerf.default 336 315 1.07:1
AttachmentMinimalPerf.default 149 141 1.06:1
HeaderMinimalPerf.default 347 326 1.06:1
AlertMinimalPerf.default 259 247 1.05:1
SkeletonMinimalPerf.default 336 321 1.05:1
TableMinimalPerf.default 386 369 1.05:1
TextMinimalPerf.default 331 316 1.05:1
AttachmentSlotsPerf.default 1021 990 1.03:1
ButtonMinimalPerf.default 159 154 1.03:1
SegmentMinimalPerf.default 320 310 1.03:1
TreeWith60ListItems.default 160 155 1.03:1
CheckboxMinimalPerf.default 2593 2549 1.02:1
FormMinimalPerf.default 380 373 1.02:1
GridMinimalPerf.default 323 318 1.02:1
ImageMinimalPerf.default 346 339 1.02:1
LabelMinimalPerf.default 370 361 1.02:1
ListNestedPerf.default 532 524 1.02:1
PopupMinimalPerf.default 568 557 1.02:1
ProviderMinimalPerf.default 959 940 1.02:1
SplitButtonMinimalPerf.default 3621 3565 1.02:1
TextAreaMinimalPerf.default 465 456 1.02:1
ToolbarMinimalPerf.default 880 865 1.02:1
AvatarMinimalPerf.default 178 176 1.01:1
CarouselMinimalPerf.default 434 430 1.01:1
ChatMinimalPerf.default 615 607 1.01:1
DropdownMinimalPerf.default 2984 2952 1.01:1
EmbedMinimalPerf.default 3971 3915 1.01:1
LoaderMinimalPerf.default 670 665 1.01:1
MenuButtonMinimalPerf.default 1558 1546 1.01:1
ReactionMinimalPerf.default 350 346 1.01:1
RefMinimalPerf.default 222 219 1.01:1
StatusMinimalPerf.default 625 617 1.01:1
IconMinimalPerf.default 581 574 1.01:1
TableManyItemsPerf.default 1763 1752 1.01:1
TooltipMinimalPerf.default 945 938 1.01:1
TreeMinimalPerf.default 733 726 1.01:1
ButtonOverridesMissPerf.default 1604 1608 1:1
DialogMinimalPerf.default 711 713 1:1
InputMinimalPerf.default 1190 1195 1:1
LayoutMinimalPerf.default 341 340 1:1
ProviderMergeThemesPerf.default 1585 1589 1:1
RadioGroupMinimalPerf.default 413 411 1:1
CustomToolbarPrototype.default 3667 3662 1:1
CardMinimalPerf.default 518 523 0.99:1
DividerMinimalPerf.default 333 336 0.99:1
FlexMinimalPerf.default 264 267 0.99:1
HeaderSlotsPerf.default 704 708 0.99:1
ItemLayoutMinimalPerf.default 1126 1133 0.99:1
ListMinimalPerf.default 481 484 0.99:1
ListWith60ListItems.default 596 603 0.99:1
AnimationMinimalPerf.default 380 387 0.98:1
DatepickerMinimalPerf.default 4989 5087 0.98:1
DropdownManyItemsPerf.default 624 635 0.98:1
MenuMinimalPerf.default 783 803 0.98:1
SliderMinimalPerf.default 1468 1494 0.98:1
ButtonSlotsPerf.default 507 521 0.97:1
ListCommonPerf.default 558 578 0.97:1
RosterPerf.default 1092 1124 0.97:1
VideoMinimalPerf.default 575 592 0.97:1
ChatDuplicateMessagesPerf.default 271 282 0.96:1
PortalMinimalPerf.default 157 171 0.92:1

```tsx
interface ChangeData<TValue, TProps> {
value: TValue;
props: TProps;
Copy link
Member

Choose a reason for hiding this comment

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

I don't see why parent props are necessary here, this is user input and we do not mutate it so why should we return all of this ?

As you mention later it will also cause some 'smoke' and make the valuable properties harder to find.

The DTO is a 👍 but we should only return data that is pertinent to the change/mutation that is happening on the component for proper encapsulation. Otherwise if consumers decide to forward change handlers internally in their app then you break encapsulation of the component tree

Copy link
Member

Choose a reason for hiding this comment

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

There are scenarios where the caller has multiple components hooked up to a single onChange callback. Without providing the original props, each instance would need to be closured with the identifiable data to handle them individually.

Also someone who looks at the event handler without reading the details might assume that props.value and value are equal.

One way to clear up some ambiguity might be to call it "originalProps" and pass along the original props. E.g. if value changes from 'a' to 'b', then value would be b, while originalProps.value would be 'a'.

Copy link
Member

Choose a reason for hiding this comment

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

each instance would need to be closured with the identifiable data to handle them individually

Isn't that a good thing ? in that case your multiple component are encapsulated into a single component and you should only pass on relevant data to its parent.

minor fix in title of option c
Copy link
Member

@ecraig12345 ecraig12345 left a comment

Choose a reason for hiding this comment

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

Thanks for doing this! I like the first option.

rfcs/convergence/event-handlers-arguments.md Outdated Show resolved Hide resolved
rfcs/convergence/event-handlers-arguments.md Outdated Show resolved Hide resolved

#### Cons

👎 `data.props.value` and `data.props.defaultValue` are accessible, leaving multiple ways to see value which might be confusing as they'll represent the current _props_ rather than the new value. But it _should be_ obvious that these are user inputs and not the new value. Could consider calling new value `newValue` to be clear, but that seems a bit unpredictable.
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is too big of an issue, especially once people get used to the pattern (since it will be consistent across all components and handlers)

#### Cons

👎 `data.props.value` and `data.props.defaultValue` are accessible, leaving multiple ways to see value which might be confusing as they'll represent the current _props_ rather than the new value. But it _should be_ obvious that these are user inputs and not the new value. Could consider calling new value `newValue` to be clear, but that seems a bit unpredictable.
👎 structure is deeply nested, for example `data.props.id`
Copy link
Member

Choose a reason for hiding this comment

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

I suspect this won't be too much of an issue either, since accessing data.props should be relatively uncommon. And if they do need to access it multiple times, they can destructure/assign it to a local const.

rfcs/convergence/event-handlers-arguments.md Outdated Show resolved Hide resolved
props: TProps;
}

const onChange = (ev: React.FormEvent, data: ChangeData<string, InputProps>) => {
Copy link
Member

Choose a reason for hiding this comment

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

What would be the pattern for typings if a particular event handler needed to add more data properties?

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO just extend ChangeData, ChangeData should not be a shared type, in reality it will look like:

// not sure about naming
type InputOnChangeData = {
  props: InputProps
  value: string
  // ... other properties on demand
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@ecraig12345 based on yours and @behowell feedback, I update this part, now it should be clearer, a0176c6. WDYT?


```tsx
interface ChangeData<TValue, TProps> {
value: TValue;
Copy link
Contributor

Choose a reason for hiding this comment

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

The name value doesn't always make sense and could possibly be confusing in some cases. E.g. for Checkbox, the onChange event happens when the checked prop changes, not the value prop. It could be more natural if either:

  1. It's named data.checked, or
  2. The event is named onCheckedChange

For option 1, this type could be:

type ChangeData<TPropName extends keyof TProps, TProps> = Pick<TProps, TPropName> & { props: TProps; }

The event would be:

onChange?: (data: ChangeData<'checked', CheckboxProps>) => void;

And it'd be used like:

<Checkbox onChange={data => console.log(data.checked)} />

Copy link
Member Author

@layershifter layershifter Aug 4, 2021

Choose a reason for hiding this comment

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

The name value doesn't always make sense and could possibly be confusing in some cases.

To be clear, there is no plan to use the same ChangeData interface for all components (#18974 (comment)), I will update this section to make clearer. Totally agree that value does not have sense for Checkbox, especially when Checkbox has checked prop.

Good call, thanks for feedback 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

@behowell I applied your feedback in a0176c6, is it clearer?

Copy link
Contributor

@bsunderhus bsunderhus left a comment

Choose a reason for hiding this comment

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

I'm in favor of Option A.

I kind of don't know how the signature would be in the case of having some handler that is not associated with a native event though (which is quite rare and probably not gonna happen)

@layershifter
Copy link
Member Author

layershifter commented Aug 9, 2021

We had an offline conversation about this RFC with the team:

  • we reached an agreement to go with "Option A1", it's modified "Option A" that does not include props
  • I updated the RFC to include this option and mark it as accepted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants