Repository: microsoft/fluent-ui-react
Pull request #2097 (2019-11-06 22:04:09)

perf: styles lazy evaluation
Previously `renderComponent` resolved styles and add CSS classes for all slots in a rendered component.

There are components which have a lot of slots (`ChatMessage` currently consists of 7 slots) but not all are always used. There are even components which have mutually exclusive slots - `checkbox` vs `toggle` slots in `Checkbox` component.
In this PR, styles and CSS classes are computed only when used (that also means only IF used).

This can bring up to 10% rendering perf improvements.

Before (master):

| Example                  | Min    | Avg    | Median | Max    |
| ------------------------ | ------ | ------ | ------ | ------ |
| Chat.perf.tsx            | 378.88 | 459.88 | 511.41 | 566.8  |
| ChatWithPopover.perf.tsx | 386.31 | 457.55 | 417.21 | 623.64 |

After (this PR):

| Example                  | Min    | Avg    | Median | Max    |
| ------------------------ | ------ | ------ | ------ | ------ |
| Chat.perf.tsx            | 312.24 | 396.36 | 438.55 | 548.93 |
| ChatWithPopover.perf.tsx | 354.09 | 413.74 | 374.05 | 497.5  |


DustyTheBot: 
<!--
  0 failure: 
  1 warning:  There are no upda...
  
  
  DangerID: danger-id-default;
-->


<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Warnings</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:warning:</td>
      <td>There are no updates provided to CHANGELOG. Ensure there are no publicly visible changes introduced by this PR.</td>
    </tr>
  </tbody>
</table>



<p align="right">
  Generated by :no_entry_sign: <a href="http://github.com/danger/danger-js/">dangerJS</a>
</p>


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3a01a9bd..2351e9786 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
 This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+
 ### Fixes
 - Update Silver color scheme, changing `backgroundHover` and `backgroundPressed` for high-contrast theme @pompomon ([#2078](https://github.com/microsoft/fluent-ui-react/pull/2078))
 - Updating the `attachment` component styles to match Teams theme @notandrew ([#2012](https://github.com/stardust-ui/react/pull/2012))
@@ -28,6 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ### Performance
 - Add rendering performance telemetry @miroslavstastny ([#2079](https://github.com/microsoft/fluent-ui-react/pull/2079))
 - Skip empty frames in mergeThemes @levithomason @miroslavstastny ([#2095](https://github.com/microsoft/fluent-ui-react/pull/2095))
+- Lazily evaluate styles and classes @levithomason @miroslavstastny ([#2097](https://github.com/microsoft/fluent-ui-react/pull/2097))
 
 ### Documentation
 - Add usage example for `Tooltip` on disabled elements @mnajdova ([#2091](https://github.com/microsoft/fluent-ui-react/pull/2091))
diff --git a/docs/src/prototypes/employeeCard/AvatarEmployeeCard.tsx b/docs/src/prototypes/employeeCard/AvatarEmployeeCard.tsx
index 69aacd7e1..9d377d219 100644
--- a/docs/src/prototypes/employeeCard/AvatarEmployeeCard.tsx
+++ b/docs/src/prototypes/employeeCard/AvatarEmployeeCard.tsx
@@ -55,7 +55,7 @@ class AvatarEmployeeCard extends React.Component<
           this.setState({ popupOpen: newProps.open })
         }}
         trigger={Avatar.create(avatar, {
-          defaultProps: {
+          defaultProps: () => ({
             name: `${firstName} ${lastName}`,
             onMouseEnter: () => {
               this.setPopupOpen(true)
@@ -63,7 +63,7 @@ class AvatarEmployeeCard extends React.Component<
             onMouseLeave: () => {
               this.setPopupOpen(false)
             },
-          },
+          }),
         })}
         content={{
           styles: { marginLeft: '10px' },
diff --git a/docs/src/prototypes/employeeCard/EmployeeCard.tsx b/docs/src/prototypes/employeeCard/EmployeeCard.tsx
index 9ae9a0f53..eac45e8b5 100644
--- a/docs/src/prototypes/employeeCard/EmployeeCard.tsx
+++ b/docs/src/prototypes/employeeCard/EmployeeCard.tsx
@@ -70,10 +70,10 @@ class EmployeeCard extends React.Component<Extendable<EmployeeCardProps>, any> {
           )}
         </div>
         {Avatar.create(avatar, {
-          defaultProps: {
+          defaultProps: () => ({
             size: 'largest',
             name: `${firstName} ${lastName}`,
-          },
+          }),
         })}
       </Grid>
     )
diff --git a/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx b/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx
index d79b5d621..36612758b 100644
--- a/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx
+++ b/docs/src/prototypes/meetingOptions/components/MSTeamsLogo.tsx
@@ -17,12 +17,12 @@ class MSTeamsLogo extends React.Component<LogoProps, any> {
           return (
             <div style={style}>
               {Icon.create(icon, {
-                defaultProps: {
+                defaultProps: () => ({
                   variables: { color: siteVariables.colors.brand[600] },
                   size: 'largest',
                   xSpacing: 'after',
                   styles: { verticalAlign: 'middle' },
-                },
+                }),
               })}
               <Text
                 styles={{
diff --git a/docs/src/prototypes/meetingOptions/components/TransparentDivider.tsx b/docs/src/prototypes/meetingOptions/components/TransparentDivider.tsx
index bf08b0a91..6857f3943 100644
--- a/docs/src/prototypes/meetingOptions/components/TransparentDivider.tsx
+++ b/docs/src/prototypes/meetingOptions/components/TransparentDivider.tsx
@@ -2,8 +2,8 @@ import { Divider as StardustUIDivider } from '@stardust-ui/react'
 
 export default props => {
   return StardustUIDivider.create(props, {
-    defaultProps: {
+    defaultProps: () => ({
       variables: { dividerColor: 'transparent' },
-    },
+    }),
   })
 }
diff --git a/packages/react/src/components/Accordion/Accordion.tsx b/packages/react/src/components/Accordion/Accordion.tsx
index cb1be526d..265b0d483 100644
--- a/packages/react/src/components/Accordion/Accordion.tsx
+++ b/packages/react/src/components/Accordion/Accordion.tsx
@@ -258,7 +258,7 @@ class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, Acco
 
       children.push(
         AccordionTitle.create(title, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Accordion.slotClassNames.title,
             active,
             index,
@@ -266,19 +266,19 @@ class Accordion extends AutoControlledComponent<WithAsProp<AccordionProps>, Acco
             canBeCollapsed,
             id: titleId,
             accordionContentId: contentId,
-          },
+          }),
           overrideProps: this.handleTitleOverrides,
           render: renderPanelTitle,
         }),
       )
       children.push(
         AccordionContent.create(content, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Accordion.slotClassNames.content,
             active,
             id: contentId,
             accordionTitleId: titleId,
-          },
+          }),
           render: renderPanelContent,
         }),
       )
diff --git a/packages/react/src/components/Accordion/AccordionTitle.tsx b/packages/react/src/components/Accordion/AccordionTitle.tsx
index fd02263f0..f30011152 100644
--- a/packages/react/src/components/Accordion/AccordionTitle.tsx
+++ b/packages/react/src/components/Accordion/AccordionTitle.tsx
@@ -119,9 +119,9 @@ class AccordionTitle extends UIComponent<WithAsProp<AccordionTitleProps>, any> {
           {...accessibility.attributes.content}
           {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.content, unhandledProps)}
           start={Icon.create(indicatorWithDefaults, {
-            defaultProps: {
+            defaultProps: () => ({
               styles: styles.indicator,
-            },
+            }),
           })}
           main={rtlTextContainer.createFor({ element: content })}
         />
diff --git a/packages/react/src/components/Alert/Alert.tsx b/packages/react/src/components/Alert/Alert.tsx
index 4bcc26666..5d7c3a253 100644
--- a/packages/react/src/components/Alert/Alert.tsx
+++ b/packages/react/src/components/Alert/Alert.tsx
@@ -180,18 +180,18 @@ class Alert extends AutoControlledComponent<WithAsProp<AlertProps>, AlertState>
     const bodyContent = (
       <>
         {Text.create(header, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Alert.slotClassNames.header,
             styles: styles.header,
             ...accessibility.attributes.header,
-          },
+          }),
         })}
         {Box.create(content, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Alert.slotClassNames.content,
             styles: styles.content,
             ...accessibility.attributes.content,
-          },
+          }),
         })}
       </>
     )
@@ -199,38 +199,38 @@ class Alert extends AutoControlledComponent<WithAsProp<AlertProps>, AlertState>
     return (
       <>
         {Icon.create(icon, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Alert.slotClassNames.icon,
             styles: styles.icon,
-          },
+          }),
         })}
         {Box.create(body, {
-          defaultProps: {
+          defaultProps: () => ({
             id: this.state.bodyId,
             className: Alert.slotClassNames.body,
             ...accessibility.attributes.body,
             styles: styles.body,
-          },
+          }),
           overrideProps: {
             children: bodyContent,
           },
         })}
 
         {ButtonGroup.create(actions, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Alert.slotClassNames.actions,
             styles: styles.actions,
-          },
+          }),
         })}
         {dismissible &&
           Button.create(dismissAction, {
-            defaultProps: {
+            defaultProps: () => ({
               iconOnly: true,
               text: true,
               className: Alert.slotClassNames.dismissAction,
               styles: styles.dismissAction,
               ...accessibility.attributes.dismissAction,
-            },
+            }),
             overrideProps: this.handleDismissOverrides,
           })}
       </>
diff --git a/packages/react/src/components/Attachment/Attachment.tsx b/packages/react/src/components/Attachment/Attachment.tsx
index 73afadaa5..0a9f54033 100644
--- a/packages/react/src/components/Attachment/Attachment.tsx
+++ b/packages/react/src/components/Attachment/Attachment.tsx
@@ -91,27 +91,27 @@ class Attachment extends UIComponent<WithAsProp<AttachmentProps>> {
       >
         {icon &&
           Icon.create(icon, {
-            defaultProps: { size: 'larger', styles: styles.icon },
+            defaultProps: () => ({ size: 'larger', styles: styles.icon }),
           })}
         {(header || description) && (
           <div className={classes.content}>
             {Text.create(header, {
-              defaultProps: { styles: styles.header },
+              defaultProps: () => ({ styles: styles.header }),
             })}
 
             {Text.create(description, {
-              defaultProps: { styles: styles.description },
+              defaultProps: () => ({ styles: styles.description }),
             })}
           </div>
         )}
         {action &&
           Button.create(action, {
-            defaultProps: {
+            defaultProps: () => ({
               iconOnly: true,
               text: true,
               styles: styles.action,
               className: Attachment.slotClassNames.action,
-            },
+            }),
           })}
         {!_.isNil(progress) && <div className={classes.progress} />}
       </ElementType>
diff --git a/packages/react/src/components/Avatar/Avatar.tsx b/packages/react/src/components/Avatar/Avatar.tsx
index be6364144..766adeed6 100644
--- a/packages/react/src/components/Avatar/Avatar.tsx
+++ b/packages/react/src/components/Avatar/Avatar.tsx
@@ -91,31 +91,31 @@ class Avatar extends UIComponent<WithAsProp<AvatarProps>, any> {
     return (
       <ElementType {...accessibility.attributes.root} {...unhandledProps} className={classes.root}>
         {Image.create(image, {
-          defaultProps: {
+          defaultProps: () => ({
             fluid: true,
             avatar: true,
             title: name,
             styles: styles.image,
-          },
+          }),
         })}
         {!image &&
           Label.create(label || {}, {
-            defaultProps: {
+            defaultProps: () => ({
               content: getInitials(name),
               circular: true,
               title: name,
               styles: styles.label,
-            },
+            }),
           })}
         {Status.create(status, {
-          defaultProps: {
+          defaultProps: () => ({
             size,
             styles: styles.status,
             variables: {
               borderColor: variables.statusBorderColor,
               borderWidth: variables.statusBorderWidth,
             },
-          },
+          }),
         })}
       </ElementType>
     )
diff --git a/packages/react/src/components/Button/Button.tsx b/packages/react/src/components/Button/Button.tsx
index 776ae7632..377a81913 100644
--- a/packages/react/src/components/Button/Button.tsx
+++ b/packages/react/src/components/Button/Button.tsx
@@ -149,7 +149,7 @@ class Button extends UIComponent<WithAsProp<ButtonProps>> {
         {!hasChildren && loading && this.renderLoader(variables, styles)}
         {!hasChildren && iconPosition !== 'after' && this.renderIcon(variables, styles)}
         {Box.create(!hasChildren && content, {
-          defaultProps: { as: 'span', styles: styles.content },
+          defaultProps: () => ({ as: 'span', styles: styles.content }),
         })}
         {!hasChildren && iconPosition === 'after' && this.renderIcon(variables, styles)}
       </ElementType>
@@ -160,11 +160,11 @@ class Button extends UIComponent<WithAsProp<ButtonProps>> {
     const { icon, iconPosition, content } = this.props
 
     return Icon.create(icon, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.icon,
         xSpacing: !content ? 'none' : iconPosition === 'after' ? 'before' : 'after',
         variables: variables.icon,
-      },
+      }),
     })
   }
 
@@ -172,10 +172,10 @@ class Button extends UIComponent<WithAsProp<ButtonProps>> {
     const { loader } = this.props
 
     return Loader.create(loader || {}, {
-      defaultProps: {
+      defaultProps: () => ({
         role: undefined,
         styles: styles.loader,
-      },
+      }),
     })
   }
 
diff --git a/packages/react/src/components/Button/ButtonGroup.tsx b/packages/react/src/components/Button/ButtonGroup.tsx
index 264422dc9..fcd18ca89 100644
--- a/packages/react/src/components/Button/ButtonGroup.tsx
+++ b/packages/react/src/components/Button/ButtonGroup.tsx
@@ -76,10 +76,10 @@ class ButtonGroup extends UIComponent<WithAsProp<ButtonGroupProps>, any> {
       <ElementType {...unhandledProps} className={classes.root}>
         {_.map(buttons, (button, idx) =>
           Button.create(button, {
-            defaultProps: {
+            defaultProps: () => ({
               circular,
               styles: this.getStyleForButtonIndex(styles, idx === 0, idx === buttons.length - 1),
-            },
+            }),
           }),
         )}
       </ElementType>
diff --git a/packages/react/src/components/Chat/Chat.tsx b/packages/react/src/components/Chat/Chat.tsx
index 243fe712c..88ee58e69 100644
--- a/packages/react/src/components/Chat/Chat.tsx
+++ b/packages/react/src/components/Chat/Chat.tsx
@@ -66,7 +66,9 @@ class Chat extends UIComponent<WithAsProp<ChatProps>, any> {
         {childrenExist(children)
           ? children
           : _.map(items, item =>
-              ChatItem.create(item, { defaultProps: { className: Chat.slotClassNames.item } }),
+              ChatItem.create(item, {
+                defaultProps: () => ({ className: Chat.slotClassNames.item }),
+              }),
             )}
       </ElementType>
     )
diff --git a/packages/react/src/components/Chat/ChatItem.tsx b/packages/react/src/components/Chat/ChatItem.tsx
index 5b56bf43a..1ae020ea7 100644
--- a/packages/react/src/components/Chat/ChatItem.tsx
+++ b/packages/react/src/components/Chat/ChatItem.tsx
@@ -91,7 +91,7 @@ class ChatItem extends UIComponent<WithAsProp<ChatItemProps>, any> {
     const gutterElement =
       gutter &&
       Box.create(gutter, {
-        defaultProps: { className: ChatItem.slotClassNames.gutter, styles: styles.gutter },
+        defaultProps: () => ({ className: ChatItem.slotClassNames.gutter, styles: styles.gutter }),
       })
 
     const messageElement = this.setAttachedPropValueForChatMessage(styles)
@@ -108,10 +108,10 @@ class ChatItem extends UIComponent<WithAsProp<ChatItemProps>, any> {
   setAttachedPropValueForChatMessage = styles => {
     const { message, attached } = this.props
     const messageElement = Box.create(message, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ChatItem.slotClassNames.message,
         styles: styles.message,
-      },
+      }),
     })
 
     // the element is ChatMessage
diff --git a/packages/react/src/components/Chat/ChatMessage.tsx b/packages/react/src/components/Chat/ChatMessage.tsx
index 0baae72e0..6c01b9a52 100644
--- a/packages/react/src/components/Chat/ChatMessage.tsx
+++ b/packages/react/src/components/Chat/ChatMessage.tsx
@@ -210,12 +210,12 @@ class ChatMessage extends UIComponent<WithAsProp<ChatMessageProps>, ChatMessageS
     const { messageNode } = this.state
 
     const actionMenuElement = Menu.create(actionMenu, {
-      defaultProps: {
+      defaultProps: () => ({
         [IS_FOCUSABLE_ATTRIBUTE]: true,
         accessibility: menuAsToolbarBehavior,
         className: ChatMessage.slotClassNames.actionMenu,
         styles: styles.actionMenu,
-      },
+      }),
     })
 
     if (!actionMenuElement) {
@@ -282,43 +282,43 @@ class ChatMessage extends UIComponent<WithAsProp<ChatMessageProps>, ChatMessageS
     const childrenPropExists = childrenExist(children)
     const className = childrenPropExists ? cx(classes.root, classes.content) : classes.root
     const badgeElement = Label.create(badge, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ChatMessage.slotClassNames.badge,
         styles: styles.badge,
-      },
+      }),
     })
 
     const reactionGroupElement = Reaction.Group.create(reactionGroup, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ChatMessage.slotClassNames.reactionGroup,
         styles: styles.reactionGroup,
-      },
+      }),
     })
 
     const actionMenuElement = this.renderActionMenu(actionMenu, styles)
 
     const authorElement = Text.create(author, {
-      defaultProps: {
+      defaultProps: () => ({
         size: 'small',
         styles: styles.author,
         className: ChatMessage.slotClassNames.author,
-      },
+      }),
     })
 
     const timestampElement = Text.create(timestamp, {
-      defaultProps: {
+      defaultProps: () => ({
         size: 'small',
         styles: styles.timestamp,
         timestamp: true,
         className: ChatMessage.slotClassNames.timestamp,
-      },
+      }),
     })
 
     const messageContent = Box.create(content, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ChatMessage.slotClassNames.content,
         styles: styles.content,
-      },
+      }),
     })
 
     return (
diff --git a/packages/react/src/components/Checkbox/Checkbox.tsx b/packages/react/src/components/Checkbox/Checkbox.tsx
index f0ceb051a..5ec87305d 100644
--- a/packages/react/src/components/Checkbox/Checkbox.tsx
+++ b/packages/react/src/components/Checkbox/Checkbox.tsx
@@ -142,10 +142,10 @@ class Checkbox extends AutoControlledComponent<WithAsProp<CheckboxProps>, Checkb
     const { label, labelPosition, icon, toggle } = this.props
 
     const labelElement = Text.create(label, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.label,
         className: Checkbox.slotClassNames.label,
-      },
+      }),
     })
 
     return (
@@ -160,13 +160,13 @@ class Checkbox extends AutoControlledComponent<WithAsProp<CheckboxProps>, Checkb
       >
         {labelPosition === 'start' && labelElement}
         {Icon.create(icon, {
-          defaultProps: {
+          defaultProps: () => ({
             outline: toggle && !this.state.checked,
             size: toggle ? 'medium' : 'smaller',
             className: Checkbox.slotClassNames.indicator,
             name: toggle ? 'stardust-circle' : 'stardust-checkmark',
             styles: toggle ? styles.toggle : styles.checkbox,
-          },
+          }),
         })}
         {labelPosition === 'end' && labelElement}
       </ElementType>
diff --git a/packages/react/src/components/Dialog/Dialog.tsx b/packages/react/src/components/Dialog/Dialog.tsx
index b8b9cee87..a2c2f3924 100644
--- a/packages/react/src/components/Dialog/Dialog.tsx
+++ b/packages/react/src/components/Dialog/Dialog.tsx
@@ -261,18 +261,18 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
       overrideProps: this.handleCancelButtonOverrides,
     })
     const confirmElement = Button.create(confirmButton, {
-      defaultProps: {
+      defaultProps: () => ({
         primary: true,
-      },
+      }),
       overrideProps: this.handleConfirmButtonOverrides,
     })
 
     const dialogActions =
       (cancelElement || confirmElement) &&
       ButtonGroup.create(actions, {
-        defaultProps: {
+        defaultProps: () => ({
           styles: styles.actions,
-        },
+        }),
         overrideProps: {
           content: (
             <Flex gap="gap.smaller">
@@ -294,29 +294,29 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
           {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.popup, unhandledProps)}
         >
           {Header.create(header, {
-            defaultProps: {
+            defaultProps: () => ({
               as: 'h2',
               className: Dialog.slotClassNames.header,
               styles: styles.header,
               ...accessibility.attributes.header,
-            },
+            }),
           })}
           {Button.create(headerAction, {
-            defaultProps: {
+            defaultProps: () => ({
               className: Dialog.slotClassNames.headerAction,
               styles: styles.headerAction,
               text: true,
               iconOnly: true,
               ...accessibility.attributes.headerAction,
-            },
+            }),
           })}
 
           {Box.create(content, {
-            defaultProps: {
+            defaultProps: () => ({
               styles: styles.content,
               className: Dialog.slotClassNames.content,
               ...accessibility.attributes.content,
-            },
+            }),
           })}
 
           {DialogFooter.create(footer, {
@@ -355,10 +355,10 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
                 }}
               >
                 {Box.create(overlay, {
-                  defaultProps: {
+                  defaultProps: () => ({
                     className: Dialog.slotClassNames.overlay,
                     styles: styles.overlay,
-                  },
+                  }),
                   overrideProps: { content: dialogContent },
                 })}
               </Ref>
diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx
index 88a718b03..f27bf36d3 100644
--- a/packages/react/src/components/Dropdown/Dropdown.tsx
+++ b/packages/react/src/components/Dropdown/Dropdown.tsx
@@ -491,11 +491,11 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
                   </div>
                   {showClearIndicator
                     ? Icon.create(clearIndicator, {
-                        defaultProps: {
+                        defaultProps: () => ({
                           className: Dropdown.slotClassNames.clearIndicator,
                           styles: styles.clearIndicator,
                           xSpacing: 'none',
-                        },
+                        }),
                         overrideProps: (predefinedProps: IconProps) => ({
                           onClick: (e: React.SyntheticEvent<HTMLElement>, iconProps: IconProps) => {
                             _.invoke(predefinedProps, 'onClick', e, iconProps)
@@ -504,13 +504,13 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
                         }),
                       })
                     : Icon.create(toggleIndicator, {
-                        defaultProps: {
+                        defaultProps: () => ({
                           className: Dropdown.slotClassNames.toggleIndicator,
                           name: 'chevron-down',
                           styles: styles.toggleIndicator,
                           outline: true,
                           size: 'small',
-                        },
+                        }),
                         overrideProps: (predefinedProps: IconProps) => ({
                           onClick: (e, indicatorProps: IconProps) => {
                             _.invoke(predefinedProps, 'onClick', e, indicatorProps)
@@ -576,14 +576,14 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
     return (
       <Ref innerRef={this.buttonRef}>
         {Button.create(triggerButton, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Dropdown.slotClassNames.triggerButton,
             content,
             id: triggerButtonId,
             fluid: true,
             styles: styles.triggerButton,
             ...restTriggerButtonProps,
-          },
+          }),
           overrideProps: (predefinedProps: IconProps) => ({
             onClick: e => {
               onClick(e)
@@ -626,13 +626,13 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
     const noPlaceholder = searchQuery.length > 0 || (multiple && value.length > 0)
 
     return DropdownSearchInput.create(searchInput || {}, {
-      defaultProps: {
+      defaultProps: () => ({
         className: Dropdown.slotClassNames.searchInput,
         placeholder: noPlaceholder ? '' : placeholder,
         inline,
         variables,
         inputRef: this.inputRef,
-      },
+      }),
       overrideProps: this.handleSearchInputOverrides(
         highlightedIndex,
         rtl,
@@ -732,7 +732,7 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
       render(item, () => {
         const selected = value.indexOf(item) !== -1
         return DropdownItem.create(item, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Dropdown.slotClassNames.item,
             active: highlightedIndex === index,
             selected,
@@ -744,7 +744,7 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
               !item.hasOwnProperty('key') && {
                 key: (item as any).header,
               }),
-          },
+          }),
           overrideProps: this.handleItemOverrides(item, index, getItemProps, selected),
           render: renderItem,
         })
@@ -755,18 +755,18 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
       ...items,
       loading &&
         ListItem.create(loadingMessage, {
-          defaultProps: {
+          defaultProps: () => ({
             key: 'loading-message',
             styles: styles.loadingMessage,
-          },
+          }),
         }),
       !loading &&
         items.length === 0 &&
         ListItem.create(noResultsMessage, {
-          defaultProps: {
+          defaultProps: () => ({
             key: 'no-results-message',
             styles: styles.noResultsMessage,
-          },
+          }),
         }),
     ]
   }
@@ -782,7 +782,7 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
     return value.map((item: DropdownItemProps, index) =>
       // (!) an item matches DropdownItemProps
       DropdownSelectedItem.create(item, {
-        defaultProps: {
+        defaultProps: () => ({
           className: Dropdown.slotClassNames.selectedItem,
           active: this.isSelectedItemActive(index),
           variables,
@@ -790,7 +790,7 @@ class Dropdown extends AutoControlledComponent<WithAsProp<DropdownProps>, Dropdo
             !item.hasOwnProperty('key') && {
               key: (item as any).header,
             }),
-        },
+        }),
         overrideProps: this.handleSelectedItemOverrides(item, rtl),
         render: renderSelectedItem,
       }),
diff --git a/packages/react/src/components/Dropdown/DropdownItem.tsx b/packages/react/src/components/Dropdown/DropdownItem.tsx
index c9648a889..450d8845a 100644
--- a/packages/react/src/components/Dropdown/DropdownItem.tsx
+++ b/packages/react/src/components/Dropdown/DropdownItem.tsx
@@ -108,32 +108,32 @@ class DropdownItem extends UIComponent<WithAsProp<DropdownItemProps>> {
         styles={styles.root}
         onClick={this.handleClick}
         header={Box.create(header, {
-          defaultProps: {
+          defaultProps: () => ({
             className: DropdownItem.slotClassNames.header,
             styles: styles.header,
-          },
+          }),
         })}
         media={Image.create(image, {
-          defaultProps: {
+          defaultProps: () => ({
             avatar: true,
             className: DropdownItem.slotClassNames.image,
             styles: styles.image,
-          },
+          }),
         })}
         content={Box.create(content, {
-          defaultProps: {
+          defaultProps: () => ({
             className: DropdownItem.slotClassNames.content,
             styles: styles.content,
-          },
+          }),
         })}
         endMedia={
           selected &&
           checkable && {
             content: Icon.create(checkableIndicator, {
-              defaultProps: {
+              defaultProps: () => ({
                 className: DropdownItem.slotClassNames.checkableIndicator,
                 styles: styles.checkableIndicator,
-              },
+              }),
             }),
             styles: styles.endMedia,
           }
diff --git a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx
index 884db0f64..1930c8920 100644
--- a/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx
+++ b/packages/react/src/components/Dropdown/DropdownSelectedItem.tsx
@@ -133,22 +133,22 @@ class DropdownSelectedItem extends UIComponent<WithAsProp<DropdownSelectedItemPr
     const { active, header, icon, image } = this.props
 
     const contentElement = Box.create(header, {
-      defaultProps: {
+      defaultProps: () => ({
         as: 'span',
         className: DropdownSelectedItem.slotClassNames.header,
         styles: styles.header,
-      },
+      }),
     })
     const renderIcon = _.isNil(icon)
       ? icon
       : render =>
           render(icon, (ComponentType, props) =>
             Icon.create(icon, {
-              defaultProps: {
+              defaultProps: () => ({
                 'aria-label': `Remove ${header} from selection.`, // TODO: Extract this in a behaviour.
                 className: DropdownSelectedItem.slotClassNames.icon,
                 styles: styles.icon,
-              },
+              }),
               overrideProps: this.handleIconOverrides(props),
             }),
           )
@@ -157,11 +157,11 @@ class DropdownSelectedItem extends UIComponent<WithAsProp<DropdownSelectedItemPr
       : render =>
           render(image, (ComponentType, props) =>
             Image.create(image, {
-              defaultProps: {
+              defaultProps: () => ({
                 avatar: true,
                 className: DropdownSelectedItem.slotClassNames.image,
                 styles: styles.image,
-              },
+              }),
               overrideProps: props,
             }),
           )
diff --git a/packages/react/src/components/Embed/Embed.tsx b/packages/react/src/components/Embed/Embed.tsx
index 8ecafa8f5..392f953d9 100644
--- a/packages/react/src/components/Embed/Embed.tsx
+++ b/packages/react/src/components/Embed/Embed.tsx
@@ -167,7 +167,7 @@ class Embed extends AutoControlledComponent<WithAsProp<EmbedProps>, EmbedState>
         {active && (
           <>
             {Video.create(video, {
-              defaultProps: {
+              defaultProps: () => ({
                 autoPlay: true,
                 controls: false,
                 loop: true,
@@ -178,15 +178,15 @@ class Embed extends AutoControlledComponent<WithAsProp<EmbedProps>, EmbedState>
                   width: variables.width,
                   height: variables.height,
                 },
-              },
+              }),
             })}
             {iframe && (
               <Ref innerRef={this.frameRef}>
                 {Box.create(iframe, {
-                  defaultProps: {
+                  defaultProps: () => ({
                     as: 'iframe',
                     styles: styles.iframe,
-                  },
+                  }),
                   overrideProps: this.handleFrameOverrides,
                 })}
               </Ref>
@@ -197,13 +197,13 @@ class Embed extends AutoControlledComponent<WithAsProp<EmbedProps>, EmbedState>
         {placeholderVisible && placeholderElement}
         {controlVisible &&
           Icon.create(control, {
-            defaultProps: {
+            defaultProps: () => ({
               className: Embed.slotClassNames.control,
               circular: true,
               name: active ? 'stardust-pause' : 'stardust-play',
               size: 'largest',
               styles: styles.control,
-            },
+            }),
           })}
       </ElementType>
     )
diff --git a/packages/react/src/components/Form/Form.tsx b/packages/react/src/components/Form/Form.tsx
index c7fe3bea4..4265d0c34 100644
--- a/packages/react/src/components/Form/Form.tsx
+++ b/packages/react/src/components/Form/Form.tsx
@@ -99,7 +99,7 @@ class Form extends UIComponent<WithAsProp<FormProps>, any> {
   renderFields = () => {
     const { fields } = this.props
     return _.map(fields, field =>
-      FormField.create(field, { defaultProps: { className: Form.slotClassNames.field } }),
+      FormField.create(field, { defaultProps: () => ({ className: Form.slotClassNames.field }) }),
     )
   }
 }
diff --git a/packages/react/src/components/Form/FormField.tsx b/packages/react/src/components/Form/FormField.tsx
index 4a1c3e8b8..ad512fe0e 100644
--- a/packages/react/src/components/Form/FormField.tsx
+++ b/packages/react/src/components/Form/FormField.tsx
@@ -85,21 +85,21 @@ class FormField extends UIComponent<WithAsProp<FormFieldProps>, any> {
     const { children, control, id, label, message, name, required, type } = this.props
 
     const labelElement = Text.create(label, {
-      defaultProps: {
+      defaultProps: () => ({
         as: 'label',
         htmlFor: id,
         styles: styles.label,
-      },
+      }),
     })
 
     const messageElement = Text.create(message, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.message,
-      },
+      }),
     })
 
     const controlElement = Box.create(control || {}, {
-      defaultProps: { required, id, name, type, styles: styles.control },
+      defaultProps: () => ({ required, id, name, type, styles: styles.control }),
     })
 
     const content = (
diff --git a/packages/react/src/components/Header/Header.tsx b/packages/react/src/components/Header/Header.tsx
index 00df5b9d4..8764f2e2e 100644
--- a/packages/react/src/components/Header/Header.tsx
+++ b/packages/react/src/components/Header/Header.tsx
@@ -84,12 +84,12 @@ class Header extends UIComponent<WithAsProp<HeaderProps>, any> {
         {rtlTextContainer.createFor({ element: contentElement, condition: !!description })}
         {!hasChildren &&
           HeaderDescription.create(description, {
-            defaultProps: {
+            defaultProps: () => ({
               className: Header.slotClassNames.description,
               variables: {
                 ...(v.descriptionColor && { color: v.descriptionColor }),
               },
-            },
+            }),
           })}
       </ElementType>
     )
diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx
index c42dbf08b..96d4f0461 100644
--- a/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx
+++ b/packages/react/src/components/HierarchicalTree/HierarchicalTree.tsx
@@ -173,13 +173,13 @@ class HierarchicalTree extends AutoControlledComponent<
 
     return _.map(items, (item: ShorthandValue<HierarchicalTreeItemProps>, index: number) =>
       HierarchicalTreeItem.create(item, {
-        defaultProps: {
+        defaultProps: () => ({
           className: HierarchicalTree.slotClassNames.item,
           index,
           exclusive,
           renderItemTitle,
           open: exclusive ? index === activeIndex : _.includes(activeIndexes, index),
-        },
+        }),
         overrideProps: this.handleTreeItemOverrides,
       }),
     )
diff --git a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx
index 54f2421a3..47a0587af 100644
--- a/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx
+++ b/packages/react/src/components/HierarchicalTree/HierarchicalTreeItem.tsx
@@ -167,24 +167,24 @@ class HierarchicalTreeItem extends UIComponent<WithAsProp<HierarchicalTreeItemPr
     return (
       <>
         {HierarchicalTreeTitle.create(title, {
-          defaultProps: {
+          defaultProps: () => ({
             className: HierarchicalTreeItem.slotClassNames.title,
             open,
             hasSubtree,
             as: hasSubtree ? 'span' : 'a',
-          },
+          }),
           render: renderItemTitle,
           overrideProps: this.handleTitleOverrides,
         })}
         {hasSubtree && open && (
           <Ref innerRef={this.treeRef}>
             {HierarchicalTree.create(items, {
-              defaultProps: {
+              defaultProps: () => ({
                 accessibility: hierarchicalSubtreeBehavior,
                 className: HierarchicalTreeItem.slotClassNames.subtree,
                 exclusive,
                 renderItemTitle,
-              },
+              }),
             })}
           </Ref>
         )}
diff --git a/packages/react/src/components/Input/Input.tsx b/packages/react/src/components/Input/Input.tsx
index 654c70c2c..227177f1c 100644
--- a/packages/react/src/components/Input/Input.tsx
+++ b/packages/react/src/components/Input/Input.tsx
@@ -141,7 +141,7 @@ class Input extends AutoControlledComponent<WithAsProp<InputProps>, InputState>
     const [htmlInputProps, restProps] = partitionHTMLProps(unhandledProps)
 
     return Box.create(wrapper, {
-      defaultProps: {
+      defaultProps: () => ({
         ...accessibility.attributes.root,
         className: cx(Input.className, className),
         children: (
@@ -153,7 +153,7 @@ class Input extends AutoControlledComponent<WithAsProp<InputProps>, InputState>
               }}
             >
               {Box.create(input || type, {
-                defaultProps: {
+                defaultProps: () => ({
                   ...htmlInputProps,
                   as: 'input',
                   type,
@@ -162,21 +162,21 @@ class Input extends AutoControlledComponent<WithAsProp<InputProps>, InputState>
                   styles: styles.input,
                   onChange: this.handleChange,
                   ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.input, htmlInputProps),
-                },
+                }),
               })}
             </Ref>
             {Icon.create(this.computeIcon(), {
-              defaultProps: {
+              defaultProps: () => ({
                 styles: styles.icon,
                 variables: variables.icon,
-              },
+              }),
               overrideProps: this.handleIconOverrides,
             })}
           </>
         ),
         styles: styles.root,
         ...restProps,
-      },
+      }),
       overrideProps: {
         as: (wrapper && (wrapper as any).as) || ElementType,
       },
diff --git a/packages/react/src/components/Label/Label.tsx b/packages/react/src/components/Label/Label.tsx
index 278f579e6..e56690bfc 100644
--- a/packages/react/src/components/Label/Label.tsx
+++ b/packages/react/src/components/Label/Label.tsx
@@ -100,16 +100,16 @@ class Label extends UIComponent<WithAsProp<LabelProps>, any> {
     }
 
     const imageElement = Image.create(image, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.image,
         variables: variables.image,
-      },
+      }),
     })
     const iconElement = Icon.create(icon, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.icon,
         variables: variables.icon,
-      },
+      }),
       overrideProps: this.handleIconOverrides,
     })
 
diff --git a/packages/react/src/components/List/List.tsx b/packages/react/src/components/List/List.tsx
index 26b632952..cfddbb271 100644
--- a/packages/react/src/components/List/List.tsx
+++ b/packages/react/src/components/List/List.tsx
@@ -161,12 +161,12 @@ class List extends AutoControlledComponent<WithAsProp<ListProps>, ListState> {
         maybeSelectableItemProps.selected = index === selectedIndex
       }
 
-      const itemProps = {
+      const itemProps = () => ({
         className: List.slotClassNames.item,
         ..._.pick(this.props, List.itemProps),
         ...maybeSelectableItemProps,
         index,
-      }
+      })
 
       return ListItem.create(item, {
         defaultProps: itemProps,
diff --git a/packages/react/src/components/List/ListItem.tsx b/packages/react/src/components/List/ListItem.tsx
index 1f04b7006..ec28da6b4 100644
--- a/packages/react/src/components/List/ListItem.tsx
+++ b/packages/react/src/components/List/ListItem.tsx
@@ -119,40 +119,40 @@ class ListItem extends UIComponent<WithAsProp<ListItemProps>> {
     const { endMedia, media, content, contentMedia, header, headerMedia } = this.props
 
     const contentElement = Box.create(content, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.content,
         styles: styles.content,
-      },
+      }),
     })
     const contentMediaElement = Box.create(contentMedia, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.contentMedia,
         styles: styles.contentMedia,
-      },
+      }),
     })
     const headerElement = Box.create(header, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.header,
         styles: styles.header,
-      },
+      }),
     })
     const headerMediaElement = Box.create(headerMedia, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.headerMedia,
         styles: styles.headerMedia,
-      },
+      }),
     })
     const endMediaElement = Box.create(endMedia, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.endMedia,
         styles: styles.endMedia,
-      },
+      }),
     })
     const mediaElement = Box.create(media, {
-      defaultProps: {
+      defaultProps: () => ({
         className: ListItem.slotClassNames.media,
         styles: styles.media,
-      },
+      }),
     })
 
     return (
diff --git a/packages/react/src/components/Loader/Loader.tsx b/packages/react/src/components/Loader/Loader.tsx
index 85bef3c89..fc1125feb 100644
--- a/packages/react/src/components/Loader/Loader.tsx
+++ b/packages/react/src/components/Loader/Loader.tsx
@@ -117,7 +117,7 @@ class Loader extends UIComponent<WithAsProp<LoaderProps>, LoaderState> {
     const { visible } = this.state
 
     const svgElement = Box.create(svg, {
-      defaultProps: { className: Loader.slotClassNames.svg, styles: styles.svg },
+      defaultProps: () => ({ className: Loader.slotClassNames.svg, styles: styles.svg }),
     })
 
     return (
@@ -128,14 +128,14 @@ class Loader extends UIComponent<WithAsProp<LoaderProps>, LoaderState> {
           {...unhandledProps}
         >
           {Box.create(indicator, {
-            defaultProps: {
+            defaultProps: () => ({
               children: svgElement,
               className: Loader.slotClassNames.indicator,
               styles: styles.indicator,
-            },
+            }),
           })}
           {Text.create(label, {
-            defaultProps: { className: Loader.slotClassNames.label, styles: styles.label },
+            defaultProps: () => ({ className: Loader.slotClassNames.label, styles: styles.label }),
           })}
         </ElementType>
       )
diff --git a/packages/react/src/components/Menu/Menu.tsx b/packages/react/src/components/Menu/Menu.tsx
index aecaae9ec..c66af2c73 100644
--- a/packages/react/src/components/Menu/Menu.tsx
+++ b/packages/react/src/components/Menu/Menu.tsx
@@ -197,7 +197,7 @@ class Menu extends AutoControlledComponent<WithAsProp<MenuProps>, MenuState> {
 
       if (kind === 'divider') {
         return MenuDivider.create(item, {
-          defaultProps: {
+          defaultProps: () => ({
             className: Menu.slotClassNames.divider,
             primary,
             secondary,
@@ -207,7 +207,7 @@ class Menu extends AutoControlledComponent<WithAsProp<MenuProps>, MenuState> {
             accessibility: accessibility.childBehaviors
               ? accessibility.childBehaviors.divider
               : undefined,
-          },
+          }),
           overrideProps: overrideDividerProps,
         })
       }
@@ -215,7 +215,7 @@ class Menu extends AutoControlledComponent<WithAsProp<MenuProps>, MenuState> {
       itemPosition++
 
       return MenuItem.create(item, {
-        defaultProps: {
+        defaultProps: () => ({
           className: Menu.slotClassNames.item,
           iconOnly,
           pills,
@@ -233,7 +233,7 @@ class Menu extends AutoControlledComponent<WithAsProp<MenuProps>, MenuState> {
           accessibility: accessibility.childBehaviors
             ? accessibility.childBehaviors.item
             : undefined,
-        },
+        }),
         overrideProps: overrideItemProps,
       })
     })
diff --git a/packages/react/src/components/Menu/MenuItem.tsx b/packages/react/src/components/Menu/MenuItem.tsx
index 1b4c9425a..9e2d9adc3 100644
--- a/packages/react/src/components/Menu/MenuItem.tsx
+++ b/packages/react/src/components/Menu/MenuItem.tsx
@@ -232,22 +232,21 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt
           {...(!wrapper && { onClick: this.handleClick })}
           {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
         >
-          {icon &&
-            Icon.create(this.props.icon, {
-              defaultProps: {
-                xSpacing: !!content ? 'after' : 'none',
-                styles: styles.icon,
-              },
-            })}
+          {Icon.create(icon, {
+            defaultProps: () => ({
+              xSpacing: !!content ? 'after' : 'none',
+              styles: styles.icon,
+            }),
+          })}
           {Box.create(content, {
-            defaultProps: { as: 'span', styles: styles.content },
+            defaultProps: () => ({ as: 'span', styles: styles.content }),
           })}
           {menu &&
             Icon.create(indicatorWithDefaults, {
-              defaultProps: {
+              defaultProps: () => ({
                 name: vertical ? 'stardust-menu-arrow-end' : 'stardust-menu-arrow-down',
                 styles: styles.indicator,
-              },
+              }),
             })}
         </ElementType>
       </Ref>
@@ -262,7 +261,7 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt
               targetRef={this.itemRef}
             >
               {Menu.create(menu, {
-                defaultProps: {
+                defaultProps: () => ({
                   accessibility: submenuBehavior,
                   className: MenuItem.slotClassNames.submenu,
                   vertical: true,
@@ -271,7 +270,7 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt
                   styles: styles.menu,
                   submenu: true,
                   indicator,
-                },
+                }),
               })}
             </Popper>
           </Ref>
@@ -281,11 +280,11 @@ class MenuItem extends AutoControlledComponent<WithAsProp<MenuItemProps>, MenuIt
 
     if (wrapper) {
       return Box.create(wrapper, {
-        defaultProps: {
+        defaultProps: () => ({
           className: cx(MenuItem.slotClassNames.wrapper, classes.wrapper),
           ...accessibility.attributes.wrapper,
           ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper),
-        },
+        }),
         overrideProps: () => ({
           children: (
             <>
diff --git a/packages/react/src/components/MenuButton/MenuButton.tsx b/packages/react/src/components/MenuButton/MenuButton.tsx
index e8463381d..abc5bcb89 100644
--- a/packages/react/src/components/MenuButton/MenuButton.tsx
+++ b/packages/react/src/components/MenuButton/MenuButton.tsx
@@ -256,10 +256,10 @@ export default class MenuButton extends AutoControlledComponent<MenuButtonProps,
     }
 
     const content = Menu.create(menu, {
-      defaultProps: {
+      defaultProps: () => ({
         ...accessibility.attributes.menu,
         vertical: true,
-      },
+      }),
       overrideProps: this.handleMenuOverrides,
     })
 
diff --git a/packages/react/src/components/Popup/Popup.tsx b/packages/react/src/components/Popup/Popup.tsx
index 9fefa35fa..1c293b89f 100644
--- a/packages/react/src/components/Popup/Popup.tsx
+++ b/packages/react/src/components/Popup/Popup.tsx
@@ -494,7 +494,7 @@ export default class Popup extends AutoControlledComponent<PopupProps, PopupStat
     const targetRef = toRefObject(mountDocument || this.context.target)
 
     const popupContent = Popup.Content.create(content || {}, {
-      defaultProps: {
+      defaultProps: () => ({
         ...(rtl && { dir: 'rtl' }),
         ...accessibility.attributes.popup,
         ...accessibility.keyHandlers.popup,
@@ -505,7 +505,7 @@ export default class Popup extends AutoControlledComponent<PopupProps, PopupStat
         pointerRef: this.pointerTargetRef,
         trapFocus,
         autoFocus,
-      },
+      }),
       overrideProps: this.getContentProps,
     })
 
diff --git a/packages/react/src/components/RadioGroup/RadioGroup.tsx b/packages/react/src/components/RadioGroup/RadioGroup.tsx
index 9fbbb01c6..8567ebee5 100644
--- a/packages/react/src/components/RadioGroup/RadioGroup.tsx
+++ b/packages/react/src/components/RadioGroup/RadioGroup.tsx
@@ -178,11 +178,11 @@ class RadioGroup extends AutoControlledComponent<WithAsProp<RadioGroupProps>, an
 
     return _.map(items, (item, index) =>
       RadioGroupItem.create(item, {
-        defaultProps: {
+        defaultProps: () => ({
           className: RadioGroup.slotClassNames.item,
           vertical,
           ...(index === 0 && isNoneValueSelected && { tabIndex: 0 }),
-        },
+        }),
         overrideProps: this.handleItemOverrides,
       }),
     )
diff --git a/packages/react/src/components/RadioGroup/RadioGroupItem.tsx b/packages/react/src/components/RadioGroup/RadioGroupItem.tsx
index 6c126e096..fdab3a174 100644
--- a/packages/react/src/components/RadioGroup/RadioGroupItem.tsx
+++ b/packages/react/src/components/RadioGroup/RadioGroupItem.tsx
@@ -136,15 +136,15 @@ class RadioGroupItem extends AutoControlledComponent<
           {...applyAccessibilityKeyHandlers(accessibility.keyHandlers.root, unhandledProps)}
         >
           {Icon.create(icon || 'stardust-circle', {
-            defaultProps: {
+            defaultProps: () => ({
               size: 'small',
               styles: styles.icon,
-            },
+            }),
           })}
           {Box.create(label, {
-            defaultProps: {
+            defaultProps: () => ({
               as: 'span',
-            },
+            }),
           })}
         </ElementType>
       </Ref>
diff --git a/packages/react/src/components/Reaction/Reaction.tsx b/packages/react/src/components/Reaction/Reaction.tsx
index fb44a1fee..e4ee81525 100644
--- a/packages/react/src/components/Reaction/Reaction.tsx
+++ b/packages/react/src/components/Reaction/Reaction.tsx
@@ -74,16 +74,16 @@ class Reaction extends UIComponent<WithAsProp<ReactionProps>> {
         ) : (
           <>
             {Icon.create(icon, {
-              defaultProps: {
+              defaultProps: () => ({
                 className: Reaction.slotClassNames.icon,
                 styles: styles.icon,
-              },
+              }),
             })}
             {Box.create(content, {
-              defaultProps: {
+              defaultProps: () => ({
                 className: Reaction.slotClassNames.content,
                 styles: styles.content,
-              },
+              }),
             })}
           </>
         )}
diff --git a/packages/react/src/components/Reaction/ReactionGroup.tsx b/packages/react/src/components/Reaction/ReactionGroup.tsx
index 7c795d149..73d931786 100644
--- a/packages/react/src/components/Reaction/ReactionGroup.tsx
+++ b/packages/react/src/components/Reaction/ReactionGroup.tsx
@@ -67,9 +67,9 @@ class ReactionGroup extends UIComponent<WithAsProp<ReactionGroupProps>> {
       <ElementType {...unhandledProps} className={classes.root}>
         {_.map(items, reaction =>
           Reaction.create(reaction, {
-            defaultProps: {
+            defaultProps: () => ({
               styles: styles.reaction,
-            },
+            }),
           }),
         )}
       </ElementType>
diff --git a/packages/react/src/components/Slider/Slider.tsx b/packages/react/src/components/Slider/Slider.tsx
index a0c8bd977..ec8bdcd10 100644
--- a/packages/react/src/components/Slider/Slider.tsx
+++ b/packages/react/src/components/Slider/Slider.tsx
@@ -192,7 +192,7 @@ class Slider extends AutoControlledComponent<WithAsProp<SliderProps>, SliderStat
             }}
           >
             {Box.create(input || type, {
-              defaultProps: {
+              defaultProps: () => ({
                 ...htmlInputProps,
                 ...accessibility.attributes.input,
                 className: Slider.slotClassNames.input,
@@ -204,7 +204,7 @@ class Slider extends AutoControlledComponent<WithAsProp<SliderProps>, SliderStat
                 value,
                 styles: styles.input,
                 ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.input, htmlInputProps),
-              },
+              }),
               overrideProps: this.handleInputOverrides,
             })}
           </Ref>
diff --git a/packages/react/src/components/SplitButton/SplitButton.tsx b/packages/react/src/components/SplitButton/SplitButton.tsx
index e0bc5bf36..299342d58 100644
--- a/packages/react/src/components/SplitButton/SplitButton.tsx
+++ b/packages/react/src/components/SplitButton/SplitButton.tsx
@@ -173,12 +173,12 @@ class SplitButton extends AutoControlledComponent<WithAsProp<SplitButtonProps>,
   }: RenderResultConfig<MenuButtonProps>): React.ReactNode {
     const { button, disabled, menu, primary, secondary, toggleButton } = this.props
     const trigger = Button.create(button, {
-      defaultProps: {
+      defaultProps: () => ({
         styles: styles.button,
         primary,
         secondary,
         disabled,
-      },
+      }),
       overrideProps: this.handleMenuButtonTriggerOverrides,
     })
 
@@ -187,7 +187,7 @@ class SplitButton extends AutoControlledComponent<WithAsProp<SplitButtonProps>,
         {MenuButton.create(
           {},
           {
-            defaultProps: {
+            defaultProps: () => ({
               accessibility: accessibility.childBehaviors
                 ? accessibility.childBehaviors.menuButton
                 : undefined,
@@ -196,12 +196,12 @@ class SplitButton extends AutoControlledComponent<WithAsProp<SplitButtonProps>,
               on: [],
               open: this.state.open,
               trigger,
-            },
+            }),
             overrideProps: this.handleMenuButtonOverrides,
           },
         )}
         {Button.create(toggleButton, {
-          defaultProps: {
+          defaultProps: () => ({
             className: SplitButton.slotClassNames.toggleButton,
             disabled,
             icon: 'stardust-arrow-down',
@@ -209,7 +209,7 @@ class SplitButton extends AutoControlledComponent<WithAsProp<SplitButtonProps>,
             primary,
             secondary,
             ...accessibility.attributes.toggleButton,
-          },
+          }),
           overrideProps: (predefinedProps: ButtonProps) => ({
             onClick: (e: React.SyntheticEvent, buttonProps: ButtonProps) => {
               _.invoke(predefinedProps, 'onClick', e, buttonProps)
diff --git a/packages/react/src/components/Status/Status.tsx b/packages/react/src/components/Status/Status.tsx
index 50c0230e5..de7678241 100644
--- a/packages/react/src/components/Status/Status.tsx
+++ b/packages/react/src/components/Status/Status.tsx
@@ -61,12 +61,12 @@ class Status extends UIComponent<WithAsProp<StatusProps>, any> {
     return (
       <ElementType className={classes.root} {...accessibility.attributes.root} {...unhandledProps}>
         {Icon.create(icon, {
-          defaultProps: {
+          defaultProps: () => ({
             size: 'smallest',
             styles: styles.icon,
             variables: variables.icon,
             xSpacing: 'none',
-          },
+          }),
         })}
       </ElementType>
     )
diff --git a/packages/react/src/components/Toolbar/Toolbar.tsx b/packages/react/src/components/Toolbar/Toolbar.tsx
index aee6f8556..147fae060 100644
--- a/packages/react/src/components/Toolbar/Toolbar.tsx
+++ b/packages/react/src/components/Toolbar/Toolbar.tsx
@@ -167,7 +167,7 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>> {
             return ToolbarRadioGroup.create(item, { overrideProps: itemOverridesFn })
           case 'toggle':
             return ToolbarItem.create(item, {
-              defaultProps: { accessibility: toggleButtonBehavior },
+              defaultProps: () => ({ accessibility: toggleButtonBehavior }),
               overrideProps: itemOverridesFn,
             })
           case 'custom':
@@ -471,9 +471,9 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>> {
     return (
       <Ref innerRef={this.overflowItemRef}>
         {ToolbarItem.create(overflowItem, {
-          defaultProps: {
+          defaultProps: () => ({
             icon: { name: 'more', outline: true },
-          },
+          }),
           overrideProps: {
             menu: this.props.overflowOpen ? this.getOverflowItems() : [],
             menuOpen: this.props.overflowOpen,
diff --git a/packages/react/src/components/Toolbar/ToolbarItem.tsx b/packages/react/src/components/Toolbar/ToolbarItem.tsx
index a0fcad6c7..88cc13304 100644
--- a/packages/react/src/components/Toolbar/ToolbarItem.tsx
+++ b/packages/react/src/components/Toolbar/ToolbarItem.tsx
@@ -257,9 +257,9 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>> {
 
     if (popup) {
       return Popup.create(popup, {
-        defaultProps: {
+        defaultProps: () => ({
           trapFocus: true,
-        },
+        }),
         overrideProps: {
           trigger: itemElement,
           children: undefined, // force-reset `children` defined for `Popup` as it collides with the `trigger`
@@ -278,11 +278,11 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>> {
 
       if (wrapper) {
         return Box.create(wrapper, {
-          defaultProps: {
+          defaultProps: () => ({
             className: cx(ToolbarItem.slotClassNames.wrapper, classes.wrapper),
             ...accessibility.attributes.wrapper,
             ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper),
-          },
+          }),
           overrideProps: predefinedProps => ({
             children: contentElement,
             onClick: e => {
diff --git a/packages/react/src/components/Toolbar/ToolbarMenu.tsx b/packages/react/src/components/Toolbar/ToolbarMenu.tsx
index 6f7de3afb..ac332cd1a 100644
--- a/packages/react/src/components/Toolbar/ToolbarMenu.tsx
+++ b/packages/react/src/components/Toolbar/ToolbarMenu.tsx
@@ -120,16 +120,16 @@ class ToolbarMenu extends UIComponent<ToolbarMenuProps> {
 
         case 'toggle':
           return ToolbarMenuItem.create(item, {
-            defaultProps: { accessibility: toolbarMenuItemCheckboxBehavior },
+            defaultProps: () => ({ accessibility: toolbarMenuItemCheckboxBehavior }),
             overrideProps: itemOverridesFn,
           })
 
         default:
           return ToolbarMenuItem.create(item, {
-            defaultProps: {
+            defaultProps: () => ({
               submenuIndicator,
               inSubmenu: submenu,
-            },
+            }),
             overrideProps: itemOverridesFn,
           })
       }
diff --git a/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx b/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx
index 46fee2711..3c498edde 100644
--- a/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx
+++ b/packages/react/src/components/Toolbar/ToolbarMenuItem.tsx
@@ -300,21 +300,23 @@ class ToolbarMenuItem extends AutoControlledComponent<
           children
         ) : (
           <>
-            {Icon.create(icon, { defaultProps: { xSpacing: !!content ? 'after' : 'none' } })}
+            {Icon.create(icon, {
+              defaultProps: () => ({ xSpacing: !!content ? 'after' : 'none' }),
+            })}
             {content}
             {active &&
               Icon.create(activeIndicator, {
-                defaultProps: {
+                defaultProps: () => ({
                   className: ToolbarMenuItem.slotClassNames.activeIndicator,
                   styles: styles.activeIndicator,
-                },
+                }),
               })}
             {menu &&
               Icon.create(submenuIndicator, {
-                defaultProps: {
+                defaultProps: () => ({
                   name: 'stardust-menu-arrow-end',
                   styles: styles.submenuIndicator,
-                },
+                }),
               })}
           </>
         )}
@@ -325,12 +327,12 @@ class ToolbarMenuItem extends AutoControlledComponent<
 
     if (popup && !hasChildren) {
       return Popup.create(popup, {
-        defaultProps: {
+        defaultProps: () => ({
           trapFocus: true,
           onOpenChange: e => {
             e.stopPropagation()
           },
-        },
+        }),
         overrideProps: {
           trigger: elementType,
           children: undefined, // force-reset `children` defined for `Popup` as it collides with the `trigger`
@@ -353,12 +355,12 @@ class ToolbarMenuItem extends AutoControlledComponent<
               >
                 <Popper align="top" position={rtl ? 'before' : 'after'} targetRef={this.itemRef}>
                   {ToolbarMenu.create(menu, {
-                    defaultProps: {
+                    defaultProps: () => ({
                       className: ToolbarMenuItem.slotClassNames.submenu,
                       styles: styles.menu,
                       submenu: true,
                       submenuIndicator,
-                    },
+                    }),
                     overrideProps: this.handleMenuOverrides(getRefs),
                   })}
                 </Popper>
@@ -378,11 +380,11 @@ class ToolbarMenuItem extends AutoControlledComponent<
     }
 
     return Box.create(wrapper, {
-      defaultProps: {
+      defaultProps: () => ({
         className: cx(ToolbarMenuItem.slotClassNames.wrapper, classes.wrapper),
         ...accessibility.attributes.wrapper,
         ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper),
-      },
+      }),
       overrideProps: () => ({
         children: (
           <>
diff --git a/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx b/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx
index 83ddd05d6..b77368688 100644
--- a/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx
+++ b/packages/react/src/components/Toolbar/ToolbarMenuRadioGroup.tsx
@@ -108,12 +108,12 @@ class ToolbarMenuRadioGroup extends UIComponent<WithAsProp<ToolbarMenuRadioGroup
       >
         {_.map(items, (item, index) =>
           ToolbarMenuItem.create(item, {
-            defaultProps: {
+            defaultProps: () => ({
               accessibility: toolbarMenuItemRadioBehavior,
               as: 'li',
               active: activeIndex === index,
               index,
-            },
+            }),
             overrideProps: this.handleItemOverrides(variables),
           }),
         )}
@@ -121,13 +121,13 @@ class ToolbarMenuRadioGroup extends UIComponent<WithAsProp<ToolbarMenuRadioGroup
     )
 
     return Box.create(wrapper, {
-      defaultProps: {
+      defaultProps: () => ({
         as: 'li',
         className: ToolbarMenuRadioGroup.slotClassNames.wrapper,
         styles: styles.wrapper,
         ...accessibility.attributes.wrapper,
         ...applyAccessibilityKeyHandlers(accessibility.keyHandlers.wrapper, wrapper),
-      },
+      }),
       overrideProps: {
         children: content,
       },
diff --git a/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx b/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx
index c73151401..0052cbdab 100644
--- a/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx
+++ b/packages/react/src/components/Toolbar/ToolbarRadioGroup.tsx
@@ -129,10 +129,10 @@ class ToolbarRadioGroup extends UIComponent<WithAsProp<ToolbarRadioGroupProps>>
       }
 
       const toolbarItem = ToolbarItem.create(item, {
-        defaultProps: {
+        defaultProps: () => ({
           accessibility: toolbarRadioGroupItemBehavior,
           active: activeIndex === index,
-        },
+        }),
         overrideProps: itemOverridesFn,
       })
 
diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx
index dc751dacd..0e6fb6914 100644
--- a/packages/react/src/components/Tooltip/Tooltip.tsx
+++ b/packages/react/src/components/Tooltip/Tooltip.tsx
@@ -283,13 +283,13 @@ export default class Tooltip extends AutoControlledComponent<TooltipProps, Toolt
     }
 
     const tooltipContent = Tooltip.Content.create(content, {
-      defaultProps: {
+      defaultProps: () => ({
         ...tooltipContentAttributes,
         open: this.state.open,
         placement,
         pointing,
         pointerRef: this.pointerTargetRef,
-      },
+      }),
       overrideProps: this.getContentProps,
     })
 
diff --git a/packages/react/src/components/Tree/Tree.tsx b/packages/react/src/components/Tree/Tree.tsx
index 86de036ae..2cdbba446 100644
--- a/packages/react/src/components/Tree/Tree.tsx
+++ b/packages/react/src/components/Tree/Tree.tsx
@@ -275,7 +275,7 @@ class Tree extends AutoControlledComponent<WithAsProp<TreeProps>, TreeState> {
           const isSubtree = hasSubtree(item)
           const isSubtreeOpen = isSubtree && this.isActiveItem(item['id'])
           const renderedItem = TreeItem.create(item, {
-            defaultProps: {
+            defaultProps: () => ({
               accessibility: accessibility.childBehaviors
                 ? accessibility.childBehaviors.item
                 : undefined,
@@ -285,7 +285,7 @@ class Tree extends AutoControlledComponent<WithAsProp<TreeProps>, TreeState> {
               key: item['id'],
               contentRef: elementRef,
               ...restItemForRender,
-            },
+            }),
             overrideProps: this.handleTreeItemOverrides,
           })
 
diff --git a/packages/react/src/components/Tree/TreeItem.tsx b/packages/react/src/components/Tree/TreeItem.tsx
index 8f0d34348..f1d5ea1a4 100644
--- a/packages/react/src/components/Tree/TreeItem.tsx
+++ b/packages/react/src/components/Tree/TreeItem.tsx
@@ -196,7 +196,7 @@ class TreeItem extends UIComponent<WithAsProp<TreeItemProps>, TreeItemState> {
     const { hasSubtree, treeSize } = this.state
 
     return TreeTitle.create(title, {
-      defaultProps: {
+      defaultProps: () => ({
         className: TreeItem.slotClassNames.title,
         open,
         hasSubtree,
@@ -207,7 +207,7 @@ class TreeItem extends UIComponent<WithAsProp<TreeItemProps>, TreeItemState> {
         accessibility: accessibility.childBehaviors
           ? accessibility.childBehaviors.title
           : undefined,
-      },
+      }),
       render: renderItemTitle,
       overrideProps: this.handleTitleOverrides,
     })
diff --git a/packages/react/src/lib/factories.ts b/packages/react/src/lib/factories.ts
index 11d0e2cc5..d9ec24c03 100644
--- a/packages/react/src/lib/factories.ts
+++ b/packages/react/src/lib/factories.ts
@@ -16,7 +16,7 @@ type ShorthandProp = 'children' | 'src' | 'type'
 
 interface CreateShorthandOptions<P> {
   /** Default props object */
-  defaultProps?: Partial<Props<P>>
+  defaultProps?: () => Partial<Props<P>>
 
   /** Override props object or function (called with regular props) */
   overrideProps?: Partial<Props<P>> | ((props: P) => Partial<Props<P>>)
@@ -194,7 +194,7 @@ function createShorthandFromValue<P>({
   // ----------------------------------------
   // Build up props
   // ----------------------------------------
-  const defaultProps = options.defaultProps || ({} as Props<P>)
+  const defaultProps = options.defaultProps ? options.defaultProps() : ({} as Props<P>)
 
   // User's props
   const usersProps =
diff --git a/packages/react/src/lib/renderComponent.tsx b/packages/react/src/lib/renderComponent.tsx
index 1c1ef5acd..01f0eb8b3 100644
--- a/packages/react/src/lib/renderComponent.tsx
+++ b/packages/react/src/lib/renderComponent.tsx
@@ -35,6 +35,7 @@ import { isEnabled as isDebugEnabled } from './debug/debugEnabled'
 import { DebugData } from './debug/debugData'
 import withDebugId from './withDebugId'
 import Telemetry from './Telemetry'
+import resolveStylesAndClasses from './resolveStylesAndClasses'
 
 export interface RenderResultConfig<P> {
   ElementType: React.ElementType<P>
@@ -241,22 +242,11 @@ const renderComponent = <P extends {}>(
     displayName, // does not affect styles, only used by useEnhancedRenderer in docs
   }
 
-  const resolvedStyles: ComponentSlotStylesPrepared = {}
-  const resolvedStylesDebug: { [key: string]: { styles: Object }[] } = {}
-  const classes: ComponentSlotClasses = {}
-
-  Object.keys(mergedStyles).forEach(slotName => {
-    resolvedStyles[slotName] = callable(mergedStyles[slotName])(styleParam)
-
-    if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
-      resolvedStylesDebug[slotName] = resolvedStyles[slotName]['_debug']
-      delete resolvedStyles[slotName]['_debug']
-    }
-
-    if (renderer) {
-      classes[slotName] = renderer.renderRule(callable(resolvedStyles[slotName]), felaParam)
-    }
-  })
+  const { resolvedStyles, resolvedStylesDebug, classes } = resolveStylesAndClasses(
+    mergedStyles,
+    styleParam,
+    renderer ? style => renderer.renderRule(() => style, felaParam) : undefined,
+  )
 
   classes.root = cx(className, classes.root, props.className)
 
@@ -271,6 +261,13 @@ const renderComponent = <P extends {}>(
     theme,
   }
 
+  let result
+  if (accessibility.focusZone) {
+    result = renderWithFocusZone(render, accessibility.focusZone, resolvedConfig)
+  } else {
+    result = render(resolvedConfig)
+  }
+
   // conditionally add sources for evaluating debug information to component
   if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
     saveDebug({
@@ -299,13 +296,6 @@ const renderComponent = <P extends {}>(
     })
   }
 
-  let result
-  if (accessibility.focusZone) {
-    result = renderWithFocusZone(render, accessibility.focusZone, resolvedConfig)
-  } else {
-    result = render(resolvedConfig)
-  }
-
   if (telemetry && telemetry.enabled) {
     const duration = performance.now() - startTime
 
diff --git a/packages/react/src/lib/resolveStylesAndClasses.ts b/packages/react/src/lib/resolveStylesAndClasses.ts
new file mode 100644
index 000000000..e05cb06e4
--- /dev/null
+++ b/packages/react/src/lib/resolveStylesAndClasses.ts
@@ -0,0 +1,77 @@
+import { ComponentSlotClasses, ComponentSlotStylesPrepared, ICSSInJSStyle } from '../themes/types'
+import { isEnabled as isDebugEnabled } from './debug/debugEnabled'
+
+// Both resolvedStyles and classes are objects of getters with lazy evaluation
+const resolveStylesAndClasses = (
+  mergedStyles: ComponentSlotStylesPrepared,
+  styleParam,
+  renderStyles,
+): {
+  resolvedStyles: ICSSInJSStyle
+  resolvedStylesDebug: { [key: string]: { styles: Object }[] }
+  classes: ComponentSlotClasses
+} => {
+  const resolvedStyles = {}
+  const resolvedStylesDebug = {}
+  const classes = {}
+
+  Object.keys(mergedStyles).forEach(slotName => {
+    // resolve/render slot styles once and cache
+    const cacheKey = `${slotName}__return`
+
+    Object.defineProperty(resolvedStyles, slotName, {
+      enumerable: false,
+      configurable: false,
+      set(val) {
+        resolvedStyles[cacheKey] = val
+        return true
+      },
+      get() {
+        if (resolvedStyles[cacheKey]) {
+          return resolvedStyles[cacheKey]
+        }
+
+        // resolve/render slot styles once and cache
+        resolvedStyles[cacheKey] = mergedStyles[slotName](styleParam)
+
+        if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
+          resolvedStylesDebug[slotName] = resolvedStyles[slotName]['_debug']
+          delete resolvedStyles[slotName]['_debug']
+        }
+
+        return resolvedStyles[cacheKey]
+      },
+    })
+
+    Object.defineProperty(classes, slotName, {
+      enumerable: false,
+      configurable: false,
+      set(val) {
+        classes[cacheKey] = val
+        return true
+      },
+      get() {
+        if (classes[cacheKey]) {
+          return classes[cacheKey]
+        }
+
+        // this resolves the getter magic
+        const styleObj = resolvedStyles[slotName]
+
+        if (renderStyles && styleObj) {
+          classes[cacheKey] = renderStyles(styleObj)
+        }
+
+        return classes[cacheKey]
+      },
+    })
+  })
+
+  return {
+    resolvedStyles,
+    resolvedStylesDebug,
+    classes,
+  }
+}
+
+export default resolveStylesAndClasses


Test patch:
diff --git a/packages/react/test/specs/lib/factories-test.tsx b/packages/react/test/specs/lib/factories-test.tsx
index 108f38646..c3da028ce 100644
--- a/packages/react/test/specs/lib/factories-test.tsx
+++ b/packages/react/test/specs/lib/factories-test.tsx
@@ -12,7 +12,7 @@ import { consoleUtil } from 'test/utils'
 
 type ShorthandConfig = {
   Component?: React.ReactType
-  defaultProps?: Props
+  defaultProps?: () => Props
   mappedProp?: string
   mappedArrayProp?: string
   overrideProps?: Props & ((props: Props) => Props) | Props
@@ -67,7 +67,9 @@ const itReturnsNull = valueOrRenderCallback => {
 const itReturnsNullGivenDefaultProps = valueOrRenderCallback => {
   test('returns null given defaultProps object', () => {
     consoleUtil.disableOnce()
-    expect(getShorthand({ valueOrRenderCallback, defaultProps: { 'data-foo': 'foo' } })).toBe(null)
+    expect(
+      getShorthand({ valueOrRenderCallback, defaultProps: () => ({ 'data-foo': 'foo' }) }),
+    ).toBe(null)
   })
 }
 
@@ -79,12 +81,15 @@ const itReturnsAValidElement = valueOrRenderCallback => {
 
 const itAppliesDefaultProps = (valueOrRenderCallback: ShorthandValue<Props>) => {
   test('applies defaultProps', () => {
-    const defaultProps = { some: 'defaults' }
+    const defaultPropsValue = { some: 'defaults' }
     const expectedResult = isValuePrimitive(valueOrRenderCallback)
-      ? { ...defaultProps, children: valueOrRenderCallback }
-      : defaultProps
+      ? { ...defaultPropsValue, children: valueOrRenderCallback }
+      : defaultPropsValue
 
-    testCreateShorthand({ valueOrRenderCallback, defaultProps }, expectedResult)
+    testCreateShorthand(
+      { valueOrRenderCallback, defaultProps: () => defaultPropsValue },
+      expectedResult,
+    )
   })
 }
 
@@ -103,7 +108,7 @@ const itMergesClassNames = (
   shorthandConfig: { valueOrRenderCallback?: ShorthandValue<Props>; mappedProp?: string },
 ) => {
   test(`merges defaultProps className and ${classNameSource} className`, () => {
-    const defaultProps = { className: 'default' }
+    const defaultProps = () => ({ className: 'default' })
     const overrideProps = { className: 'override' }
 
     let expectedClassNames = 'default override'
@@ -143,7 +148,7 @@ const mappedProps = {
 
 const itOverridesDefaultPropsWithFalseyProps = (propsSource, shorthandConfig) => {
   test(`overrides defaultProps with falsey ${propsSource} props`, () => {
-    const defaultProps = { undef: '-', nil: '-', zero: '-', empty: '-' }
+    const defaultProps = () => ({ undef: '-', nil: '-', zero: '-', empty: '-' })
     const expectedProps = { undef: undefined, nil: null, zero: 0, empty: '' }
 
     testCreateShorthand({ defaultProps, ...shorthandConfig }, expectedProps)
@@ -225,9 +230,9 @@ describe('factories', () => {
           getShorthand({
             valueOrRenderCallback,
             Component: 'div',
-            defaultProps: {
+            defaultProps: () => ({
               baz: 'original',
-            },
+            }),
             overrideProps: {
               baz: 'overriden',
             },
@@ -281,12 +286,12 @@ describe('factories', () => {
       test('deep merges styles prop onto defaultProps styles', () => {
         expect.assertions(1)
 
-        const defaultProps = {
+        const defaultProps = () => ({
           styles: {
             color: 'override me',
             ':hover': { color: 'blue' },
           },
-        }
+        })
         const props = {
           styles: { color: 'black' },
         }
@@ -346,12 +351,12 @@ describe('factories', () => {
       test('deep merges styles prop as function onto defaultProps styles', () => {
         expect.assertions(1)
 
-        const defaultProps = {
+        const defaultProps = () => ({
           styles: () => ({
             color: 'override me',
             ':hover': { color: 'blue' },
           }),
-        }
+        })
         const props = {
           styles: { color: 'black' },
         }
@@ -411,10 +416,10 @@ describe('factories', () => {
 
     describe('defaultProps', () => {
       test('can be an object', () => {
-        const defaultProps = { 'data-some': 'defaults' }
+        const defaultPropsValue = { 'data-some': 'defaults' }
         testCreateShorthand(
-          { defaultProps, valueOrRenderCallback: 'foo' },
-          { ...defaultProps, children: 'foo' },
+          { defaultProps: () => defaultPropsValue, valueOrRenderCallback: 'foo' },
+          { ...defaultPropsValue, children: 'foo' },
         )
       })
     })
@@ -532,20 +537,32 @@ describe('factories', () => {
       })
 
       test("is called with the user's element's and default props", () => {
-        const defaultProps = { 'data-some': 'defaults' }
+        const defaultPropsValue = { 'data-some': 'defaults' }
         const overrideProps = jest.fn(() => ({}))
 
-        shallow(getShorthand({ defaultProps, overrideProps, valueOrRenderCallback: <div /> }))
-        expect(overrideProps).toHaveBeenCalledWith(defaultProps)
+        shallow(
+          getShorthand({
+            defaultProps: () => defaultPropsValue,
+            overrideProps,
+            valueOrRenderCallback: <div />,
+          }),
+        )
+        expect(overrideProps).toHaveBeenCalledWith(defaultPropsValue)
       })
 
       test("is called with the user's props object", () => {
-        const defaultProps = { 'data-some': 'defaults' }
+        const defaultPropsValue = { 'data-some': 'defaults' }
         const overrideProps = jest.fn(() => ({}))
         const userProps = { 'data-user': 'props' }
 
-        shallow(getShorthand({ defaultProps, overrideProps, valueOrRenderCallback: userProps }))
-        expect(overrideProps).toHaveBeenCalledWith({ ...defaultProps, ...userProps })
+        shallow(
+          getShorthand({
+            defaultProps: () => defaultPropsValue,
+            overrideProps,
+            valueOrRenderCallback: userProps,
+          }),
+        )
+        expect(overrideProps).toHaveBeenCalledWith({ ...defaultPropsValue, ...userProps })
       })
     })
 
@@ -587,7 +604,7 @@ describe('factories', () => {
       )
       itOverridesDefaultProps(
         'mappedProp',
-        { some: 'defaults', overridden: null },
+        () => ({ some: 'defaults', overridden: null }),
         { some: 'defaults', overridden: <div /> },
         {
           valueOrRenderCallback: <div />,
@@ -615,7 +632,7 @@ describe('factories', () => {
 
       itOverridesDefaultProps(
         'mappedProp',
-        { some: 'defaults', overridden: 'false' },
+        () => ({ some: 'defaults', overridden: 'false' }),
         { some: 'defaults', overridden: 'true' },
         {
           valueOrRenderCallback: 'true',
@@ -633,7 +650,7 @@ describe('factories', () => {
           describe(`'${as}' as 'as' prop to defaultProps`, () => {
             test(`overrides ${mappedProp} and ${testMsg}`, () => {
               testCreateShorthand(
-                { mappedProp, valueOrRenderCallback: value, defaultProps: { as } },
+                { mappedProp, valueOrRenderCallback: value, defaultProps: () => ({ as }) },
                 { as, [mappedProps[as]]: value },
               )
             })
@@ -654,7 +671,7 @@ describe('factories', () => {
                 {
                   mappedProp,
                   valueOrRenderCallback: value,
-                  defaultProps: { as: 'overriden' },
+                  defaultProps: () => ({ as: 'overriden' }),
                   overrideProps: { as },
                 },
                 { as, [mappedProps[as]]: value },
@@ -670,7 +687,11 @@ describe('factories', () => {
         describe(`and an unsupported tag as 'as' prop to defaultProps`, () => {
           test(testMsg, () => {
             testCreateShorthand(
-              { mappedProp, valueOrRenderCallback: value, defaultProps: { as: 'unsupported' } },
+              {
+                mappedProp,
+                valueOrRenderCallback: value,
+                defaultProps: () => ({ as: 'unsupported' }),
+              },
               { as: 'unsupported', [mappedProp]: value },
             )
           })
@@ -691,7 +712,7 @@ describe('factories', () => {
               {
                 mappedProp,
                 valueOrRenderCallback: value,
-                defaultProps: { as: 'div' },
+                defaultProps: () => ({ as: 'div' }),
                 overrideProps: { as: 'unsupported' },
               },
               { as: 'unsupported', [mappedProp]: value },
@@ -706,7 +727,7 @@ describe('factories', () => {
         describe(`and an unsupported tag as 'as' prop to defaultProps`, () => {
           test(testMsg, () => {
             testCreateShorthand(
-              { valueOrRenderCallback: value, defaultProps: { as: 'unsupported' } },
+              { valueOrRenderCallback: value, defaultProps: () => ({ as: 'unsupported' }) },
               { as: 'unsupported', children: value },
             )
           })
@@ -726,7 +747,7 @@ describe('factories', () => {
             testCreateShorthand(
               {
                 valueOrRenderCallback: value,
-                defaultProps: { as: 'div' },
+                defaultProps: () => ({ as: 'div' }),
                 overrideProps: { as: 'unsupported' },
               },
               { as: 'unsupported', children: value },
@@ -746,7 +767,7 @@ describe('factories', () => {
 
       itOverridesDefaultProps(
         'props object',
-        { some: 'defaults', overridden: false },
+        () => ({ some: 'defaults', overridden: false }),
         { some: 'defaults', overridden: true },
         {
           valueOrRenderCallback: { overridden: true },
@@ -771,7 +792,7 @@ describe('factories', () => {
               {
                 mappedArrayProp,
                 valueOrRenderCallback: value,
-                defaultProps: { as: 'unsupported' },
+                defaultProps: () => ({ as: 'unsupported' }),
               },
               { as: 'unsupported', [mappedArrayProp]: value },
             )
@@ -782,7 +803,7 @@ describe('factories', () => {
 
     describe('style', () => {
       test('merges style prop', () => {
-        const defaultProps = { style: { left: 5 } }
+        const defaultProps = () => ({ style: { left: 5 } })
         const userProps = { style: { bottom: 5 } }
         const overrideProps = { style: { right: 5 } }
 
@@ -794,7 +815,7 @@ describe('factories', () => {
       })
 
       test('merges style prop and handles override by userProps', () => {
-        const defaultProps = { style: { left: 10, bottom: 5 } }
+        const defaultProps = () => ({ style: { left: 10, bottom: 5 } })
         const userProps = { style: { bottom: 10 } }
 
         expect(
@@ -818,7 +839,7 @@ describe('factories', () => {
       })
 
       test('merges style prop from defaultProps and overrideProps', () => {
-        const defaultProps = { style: { left: 10, bottom: 5 } }
+        const defaultProps = () => ({ style: { left: 10, bottom: 5 } })
         const overrideProps = { style: { bottom: 10 } }
 
         expect(
diff --git a/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts b/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts
new file mode 100644
index 000000000..f2580aa0f
--- /dev/null
+++ b/packages/react/test/specs/lib/resolveStylesAndClasses-test.ts
@@ -0,0 +1,58 @@
+import resolveStylesAndClasses from 'src/lib/resolveStylesAndClasses'
+import { ComponentSlotStylesPrepared } from 'src/themes/types'
+
+describe('resolveStylesAndClasses', () => {
+  const styleParam = {
+    variables: {
+      color: 'red',
+    },
+  }
+
+  const componentStyles: ComponentSlotStylesPrepared = {
+    root: ({ variables }) => ({
+      color: variables['color'],
+    }),
+  }
+
+  test('resolves styles', () => {
+    const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, () => ({}))
+
+    expect(resolvedStyles.root).toMatchObject({ color: 'red' })
+  })
+
+  test('caches resolved styles', () => {
+    spyOn(componentStyles, 'root').and.callThrough()
+    const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, () => ({}))
+
+    expect(resolvedStyles.root).toMatchObject({ color: 'red' })
+    expect(componentStyles.root).toHaveBeenCalledTimes(1)
+    expect(resolvedStyles.root).toMatchObject({ color: 'red' })
+    expect(componentStyles.root).toHaveBeenCalledTimes(1)
+  })
+
+  test('does not render classes if not fetched', () => {
+    const renderStyles = jest.fn()
+    const { resolvedStyles } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles)
+
+    expect(resolvedStyles.root).toMatchObject({ color: 'red' })
+    expect(renderStyles).not.toBeCalled()
+  })
+
+  test('renders classes when slot classes getter is accessed', () => {
+    const renderStyles = jest.fn().mockReturnValue('a')
+    const { classes } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles)
+
+    expect(classes.root).toBeDefined()
+    expect(renderStyles).toHaveBeenCalledWith({ color: 'red' })
+  })
+
+  test('caches rendered classes', () => {
+    const renderStyles = jest.fn().mockReturnValue('a')
+    const { classes } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles)
+
+    expect(classes.root).toBeDefined()
+    expect(renderStyles).toHaveBeenCalledWith({ color: 'red' })
+    expect(classes.root).toBeDefined()
+    expect(renderStyles).toHaveBeenCalledTimes(1)
+  })
+})