-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Slots: Refactor and add options structure for more rendering options. #8754
Conversation
packages/experiments/src/components/CollapsibleSection/CollapsibleSectionTitle.ts
Outdated
Show resolved
Hide resolved
...riments/src/components/CollapsibleSection/examples/CollapsibleSection.Customized.Example.tsx
Outdated
Show resolved
Hide resolved
packages/experiments/src/slots/examples/Slots.Stack.Example.tsx
Outdated
Show resolved
Hide resolved
Component perf results:
|
// TODO: This cast is required because menu is required in IMenuButtonSlots. | ||
// However, it's provided by the top level props of ISplitRibbonMenuButton props, so it shouldn't be required in multiple places. | ||
// Should menu be made optional in IMenuButtonSlots? | ||
const verticalMenuButtonProps: IRibbonMenuButtonProps = { content: props.content, vertical: true } as IRibbonMenuButtonProps; |
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.
@khmakoto Curious what you think of this. menu
is provided in the top level props. Having it required in the props also requires it at the slot level any time any props are provided for the slot. I'm not sure best how to solve this yet, but I'm wondering if having menu
be optional is one solution.
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, I think my idea originally was to have menu
be required for the MenuButton
given that we were separating it from the regular Button
. But now that we're using MenuButton
as a building block for other components (and which is the real reason to use foundation in the end) then I think having menu
be optional is a good solution. We should just check that everything still works as intended.
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.
Do you want to enter an item for this or is the change marked todo? Thanks!
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've marked it as a to-do in #6134.
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.
Is there any work left to do on this PR? If not, I can approve it, but I want to make sure I don't approve it prematurely.
packages/experiments/src/components/Button/examples/Button.Slots.Example.tsx
Outdated
Show resolved
Hide resolved
packages/experiments/src/components/Button/examples/Button.Slots.Example.tsx
Outdated
Show resolved
Hide resolved
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.
The Text and Announced example changes look good
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.
LGTM 👍
Hi, all. Passing an object to React element will break the shallow compare for optimization. For the Existing: <Button
icon={{
render: (iconProps, DefaultComponent) => (
<b>
Icon: <DefaultComponent {...iconProps} iconName="upload" />
</b>
)
}}
content="Icon: Function, Text + Icon"
/> Why not? // Declare in a static place to avoid re-create on each run.
function renderIcon(iconProps, DefaultComponent): JSX.Element (
<b>
Icon: <DefaultComponent {...iconProps} iconName="upload" />
</b>
)
// Pass the shallow compare check.
<Button
renderIcon={renderIcon}
content="Icon: Function, Text + Icon"
/> |
Slots props objects are not passed directly into |
We've also decided to consolidate all of these types of properties (render function, component implementation, props objects, and shorthand) because traditionally we have flattened props, which has caused prop interface explosion and inconsistent naming and implementations across our component library. |
Well, the nest props are breaking the shallow comparison on the first level optimization. Give an example, I have a component named function App() {
const [inputValue, setInputValue] = React.useState('');
return (
<fomr>
<input value={inputValue} onChange={setInputValue} />
<FabricReact.IconButton
icon={{ props: { iconName: 'Submit' } }}
label='Submit'
/>
<form>
);
} If we use something like this, the function renderIcon(DefaultIcon, defaultProps): JSX {
return (<DefaultIcon {...defaultProps} iconName='Submit' />);
}
function App() {
const [inputValue, setInputValue] = React.useState('');
return (
<fomr>
<input value={inputValue} onChange={setInputValue} />
<FabricReact.IconButton
renderIcon={renderIcon}
label='Submit'
/>
<form>
);
} |
Correct me if I'm wrong, but I believe this only matters when:
Example here: https://codesandbox.io/s/pm6zy9z400 Props don't mutate, but just by having children, it makes the "pure" or "memo" useless. So at least from my perspective for pure to work well, where you actually avoid re-rendering as a result of props not changing, you need the rules above (especially 2) to apply. So something like |
If you want to bake the The slots equivalent is more similar to your optimized example: const iconSlot = {
render: (DefaultIcon, defaultProps): JSX => {
return (<DefaultIcon {...defaultProps} iconName='Submit' />);
}
};
function App() {
const [inputValue, setInputValue] = React.useState('');
return (
<form>
<input value={inputValue} onChange={setInputValue} />
<FabricReact.IconButton
icon={iconSlot}
label='Submit'
/>
<form>
);
} |
What @JasonGore said should avoid rerenders, assuming IconButton is pure. Another thing we've been discussing is that the Pure components are certainly fragile and easy to mess up on, and I hear the concern that any complex object prop type, or functional prop type, or even children, could cause pure components to always rerender. |
@dzearing @JasonGore I think all of your points are correct. I think the core problem is the balance of the abstraction and customization. Take contextual menu as an example. Here is one screenshot of the contextual menu: Think about how it is created from JSX tree. It may be something like this: function MyContextualMenu(): JSX.Element {
return (
<>
<ToggleButton text="Click for ContextualMenu" />
<Popup>
<MenuItem text="New" />
<MenuItemBar />
<MenuItem text="Rename" />
<MenuItem text="Edit" />
<MenuItem text="Properties" />
<MenuItem text="Link same window" />
</Popup>
</>
);
} Then, we want make a component named interface IContextualMenuProps {
text: string;
menuItems: { text: string; key: React.Key; }[];
} Compare the props interface with the JSX tree, they looks very similar. Although we want to make an abstraction layer, but it actually does not work. Worse, if we are supporting more scenario like, submenu or icon for menu item. We are extending the interface props just as the JSX tree. interface IContextualMenuItem {
key: React.Key;
text: string;
icon?: string;
children: IConextualMenuItem[];
}
interface IContextualMenuProps {
text: string;
menuItems: IContextualMenuItem[];
} If I really understand why the props is structured like this, I know how the JSX tree is rendered. So, there are choices - do you want to write this huge complicated props object, or a controlled JSX tree? The complexity of the component leads the complexity of the component props interface. This is exactly one case of leaky abstraction. It is not a fault of pure component, it is solving the problem to configure everything in JSX tree from props. |
David and I were talking about this quite a bit. The line between "what to render" and "how to render" is not black and white, particularly when you consider props like I hope we all agree that mixing in "how to render" implementations is unavoidable: there is a lot of demand for render functions and replaceable subcomponents. As a result, I think this is primarily a matter of what the props interface looks like. I've been thinking of a couple of ideas and have prototyped one already. One option is to limit "how" to component creation (bind time) so that it doesn't mix in with render functions, but I worry that's a bit too limiting. Another option is to separate out the "how" from the "what" props with a new const iconSlot = {
render: (DefaultIcon, defaultProps): JSX => {
return (<DefaultIcon {...defaultProps} />);
}
};
function App() {
const [inputValue, setInputValue] = React.useState('');
return (
<form>
<input value={inputValue} onChange={setInputValue} />
<FabricReact.IconButton
icon={{ iconName: 'Submit' }} // "what" to render
label='Submit'
slots={{ icon: iconSlot }} // "how" to render it
/>
<form>
);
} What do you think? |
I totally agree. We are kinds of exposing the details about how to render - that is unavoidable. I don't think we can/should solve the problem to hide everything inside one component.
I could think from another angle. First, there are always different requirements on one component - w/o icon, w/o image, different font-size, margin/padding, etc. I think we should solve the problem about what if the one-stop component cannot fit the needs, what is the easiest way to let consumers quickly unblock themselves. What is the current answer for this problem? The consumer needs to:
This process is very long and risky. If finally the consumer finds the new prop/implementation does not fit the needs at step 4, the consumer needs to start over the whole process again. Besides, sometimes the step 3 is very hard, with version upgrade, it will introduce unwanted changes on unwanted components, sometimes it is hard to fix. What I propose: create component with renderers (HoC, hooks, decorators, etc.) So the complicated components are integrations of renders and small components - with a few lines of codes. If the complicated components cannot fit the needs, the consumers can directly copy paste those few lines of codes to start customization. With a better situation, if there are many copy-paste on one very common scenario, fabric-react can normalize that requirement as one-stop component.
First of all, your options are solving another problem different than my one described above. But, if I am making judgements on the options to solve my above problem, I could like to choose the how to render way. Because that gives more rooms for customization. Besides, only providing how to render is not enough, the complicated component needs to be flat (with few lines of code) - it is unacceptable to copy-paste hundreds lines of code. |
BTW, @dzearing mentioned there is a |
Our slots work is intended to fix exactly the problem you just mentioned: each component is composed of simple, atomic functional regions which are called slots. Any user of said component can completely replace or supplement any or all slots with the own render functions and components. The goal is to make customization easy and remove the need for bulky components that have to solve every scenario. We are already working on a new version of Button that has a simple Button core with variants along the lines of what you describe that add Split and Menu functionality as separate variants using slots. The API changes in this PR directly affect Slot props can also be specified at binding time, the idea being that what you wrote above could be a new component variant also written like (tweaked to show a hypothetical where a Button that didn't have an Icon could have an IconButton variant): // Let's create a component variant based on a Fabric component, or even another variant.
// extendComponent should allow users to easily create variants with just a few lines of code, like this:
const IconButton = extendComponent(Button,
content: {
render: (props, DefaultComponent): JSX => {
return (
<>
<Icon iconName={props.iconName} />
<DefaultComponent {...props} />);
</>
);
}
}
};
function App() {
const [inputValue, setInputValue] = React.useState('');
return (
<form>
<input value={inputValue} onChange={setInputValue} />
<IconButton content={{ iconName: 'Submit' }} /> // type safety will be the trick here
<form>
);
} This all being said, I thought you were highlighting concerns with the slots API. These slot props don't have to be hidden inside of bulky components and their render functions, they can also be used to create component variants as shown above. If I'm still not addressing your concerns, maybe a call or meeting would be the next best step. 😄 |
It is great to see that, my concerns have already been considered! 👍 However, I am not sure how slots are solving the customization problem. I read the slots implementation in this pull request, but not really understand it. Besides, during looking on that PR, I have one question about this code: const Slots = getSlots<typeof props, IButtonSlots>(props, {
root: _deriveRootType(props),
stack: Stack,
icon: Icon,
content: Text,
menu: ContextualMenu,
menuIcon: Icon
});
return (
<Slots.root {...} />
); Is the |
Regarding the customization problem, I gave an example of how an
|
🎉 Handy links: |
Pull request checklist
$ npm run change
Description of changes
ISlotOptions
, giving slots a powerful mechanism for adding features over time without breaking backwards compatibility.render
function that still allows implementations to wrap default content without the addedrender
argument.component
option that allows a user to simply provide a component implementation taking in the slot props.ISlotProp
signature. It is now a union of shorthand (string, number or boolean) andISlotOptions
, shown here consolidated and simplified:Example Improvements
To do a customized CollapsibleSection with tooltip and icon such as this:
Before:
Previously required a render function and a closure just to capture user props:
After:
Now can be done with just a props object and component:
Before:
And even simpler scenarios where just the component implementation is changed:
After:
Now can just use
component
and automatically have props applied.And render functions are now also easier to use, while still allowing the capability to wrap default render content:
Before:
After:
I found multiple TS issues while doing this PR that unfortunately have impacted this PR. These are items impacted:
props
definitions as well asprops
use incomponent
andrender
where excess (mistyped) prop names do not error.TProps
used withISlotProps
(currently allows functions)props
for ease of use, which would be beneficial primarily whencomponent
andrender
are also provided.Related TS PRs / issues:
Focus areas to test
Add unit tests and typing tests for Foundation.
Microsoft Reviewers: Open in CodeFlow