Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(useStyles): add caching when no inline overrides are applied #2309

Merged
merged 39 commits into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
53ae41b
wip
mnajdova Feb 3, 2020
f90e31a
cleanup
mnajdova Feb 3, 2020
4ac7bfd
-prettier fixes
mnajdova Feb 3, 2020
db1e7aa
-disabled tests
mnajdova Feb 3, 2020
77d0748
-test updates
mnajdova Feb 3, 2020
f213a4e
-test fixes
mnajdova Feb 4, 2020
693d237
wip
layershifter Feb 4, 2020
d7be310
Merge branch 'master' into feat/add-style-caching
mnajdova Feb 4, 2020
e4c7fb8
-fixes
mnajdova Feb 4, 2020
6372814
wip
mnajdova Feb 4, 2020
f7fbcd1
prettier fixes
mnajdova Feb 5, 2020
84eb6d4
prettier fixes
mnajdova Feb 5, 2020
7d45f26
-fixes in resolveStylesAndCLasses
mnajdova Feb 5, 2020
6083215
-removed variables cache from Provider's context
mnajdova Feb 5, 2020
3d7732d
-prettier fixes
mnajdova Feb 5, 2020
eb389d5
-added provider flag for enabling caching
mnajdova Feb 7, 2020
53bab76
-prettier fixes
mnajdova Feb 7, 2020
6f3b551
Merge branch 'master' into feat/add-style-caching
mnajdova Feb 7, 2020
214a6ac
-addressing comments
mnajdova Feb 7, 2020
f220251
-addressing comments
mnajdova Feb 7, 2020
0a308bd
prettier fixes
mnajdova Feb 7, 2020
e1f75d3
-updated comments
mnajdova Feb 7, 2020
399e8ac
-refactored key generation
mnajdova Feb 7, 2020
d403c1e
-prettier fixes
mnajdova Feb 7, 2020
697ca82
-added one Provider's prop performance
mnajdova Feb 7, 2020
0b970c4
-moved PrimitiveProps typings
mnajdova Feb 7, 2020
9dae249
prettier fixes
mnajdova Feb 7, 2020
cfb0462
-fixed unit tests
mnajdova Feb 10, 2020
e951ba8
-renamed resolveStylesAndClasses to resolveStyles and merged two func…
mnajdova Feb 10, 2020
ac92aa9
-prettier fixes
mnajdova Feb 10, 2020
40712af
-added component variables cache
mnajdova Feb 10, 2020
f6db0e7
-updated params for resolveStyles
mnajdova Feb 10, 2020
1b91683
-addressed final comments
mnajdova Feb 10, 2020
79f2ce2
prettier fixes
mnajdova Feb 10, 2020
6eea045
Merge branch 'master' into feat/add-style-caching
mnajdova Feb 10, 2020
7b84579
-fixed typings
mnajdova Feb 10, 2020
c03fe01
-updated changelog
mnajdova Feb 10, 2020
3111bbd
-improved tests
mnajdova Feb 10, 2020
d89bf0e
-fixed changelog
mnajdova Feb 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/react-bindings/src/hooks/useStyles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
ComponentSlotStyle,
ComponentSlotStylesPrepared,
ComponentSlotStylesResolved,
ComponentVariablesInput,
DebugData,
emptyTheme,
Expand All @@ -27,7 +27,7 @@ type UseStylesOptions<StyleProps extends PrimitiveProps> = {

type UseStylesResult = {
classes: ComponentSlotClasses
styles: ComponentSlotStylesPrepared
styles: ComponentSlotStylesResolved
}

type InlineStyleProps<StyleProps> = {
Expand Down Expand Up @@ -82,6 +82,7 @@ const useStyles = <StyleProps extends PrimitiveProps>(
saveDebug: fluentUIDebug => (debug.current = { fluentUIDebug }),
theme: context.theme,
_internal_resolvedComponentVariables: context._internal_resolvedComponentVariables,
__experimental_cache: true,
mnajdova marked this conversation as resolved.
Show resolved Hide resolved
})

return { classes, styles: resolvedStyles }
Expand Down
190 changes: 141 additions & 49 deletions packages/react-bindings/src/styles/getStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
callable,
ComponentSlotStylesInput,
ComponentSlotStylesPrepared,
ComponentSlotStylesResolved,
ComponentStyleFunctionParam,
ComponentVariablesObject,
DebugData,
Expand All @@ -10,12 +11,13 @@ import {
mergeComponentStyles,
mergeComponentVariables,
PropsWithVarsAndStyles,
ThemePrepared,
withDebugId,
} from '@fluentui/styles'
import cx from 'classnames'
import * as _ from 'lodash'

import resolveStylesAndClasses from './resolveStylesAndClasses'
import resolveStylesAndClasses, { ResolveStylesResult } from './resolveStylesAndClasses'
import {
ComponentDesignProp,
ComponentSlotClasses,
Expand All @@ -32,18 +34,22 @@ type GetStylesOptions = StylesContextValue<{
props: PropsWithVarsAndStyles & { design?: ComponentDesignProp }
rtl: boolean
saveDebug: (debug: DebugData | null) => void

__experimental_cache?: boolean
}

export type GetStylesResult = {
classes: ComponentSlotClasses
variables: ComponentVariablesObject
styles: ComponentSlotStylesPrepared
styles: ComponentSlotStylesResolved
theme: StylesContextValue['theme']
}

const stylesCache = new WeakMap<ThemePrepared, Record<string, ResolveStylesResult>>()

const getStyles = (options: GetStylesOptions): GetStylesResult => {
const {
className,
className: componentClassName,
disableAnimations,
displayName,
props,
Expand All @@ -52,70 +58,93 @@ const getStyles = (options: GetStylesOptions): GetStylesResult => {
saveDebug,
theme,
_internal_resolvedComponentVariables: resolvedComponentVariables,
__experimental_cache: cacheEnabled,
} = options

// Resolve variables for this component, cache the result in provider
if (!resolvedComponentVariables[displayName]) {
resolvedComponentVariables[displayName] =
callable(theme.componentVariables[displayName])(theme.siteVariables) || {} // component variables must not be undefined/null (see mergeComponentVariables contract)
}
const { className, design, styles, variables, ...restProps } = props

// Merge inline variables on top of cached variables
const resolvedVariables = props.variables
? mergeComponentVariables(
resolvedComponentVariables[displayName],
withDebugId(props.variables, 'props.variables'),
)(theme.siteVariables)
: resolvedComponentVariables[displayName]
const componentKey = displayName
const noInlineOverrides = !(design || styles || variables)

// Resolve styles using resolved variables, merge results, allow props.styles to override
let mergedStyles: ComponentSlotStylesPrepared = theme.componentStyles[displayName] || {
root: () => ({}),
}
//
// VARIABLES
//

const hasInlineOverrides = !_.isNil(props.design) || !_.isNil(props.styles)
let resolvedVariables: object

if (hasInlineOverrides) {
mergedStyles = mergeComponentStyles(
mergedStyles,
props.design && withDebugId({ root: props.design }, 'props.design'),
props.styles &&
withDebugId({ root: props.styles } as ComponentSlotStylesInput, 'props.styles'),
)
// Resolve variables for this component, cache the result in provider
if (!resolvedComponentVariables[componentKey]) {
resolvedComponentVariables[componentKey] =
callable(theme.componentVariables[componentKey])(theme.siteVariables) || {} // component variables must not be undefined/null (see mergeComponentVariables contract)
}

const styleParam: ComponentStyleFunctionParam = {
displayName,
props,
variables: resolvedVariables,
theme,
rtl,
disableAnimations,
if (cacheEnabled && noInlineOverrides) {
resolvedVariables = resolvedComponentVariables[componentKey]
} else {
//
// Old caching of variables
//

// Merge inline variables on top of cached variables
resolvedVariables = props.variables
? mergeComponentVariables(
resolvedComponentVariables[componentKey],
withDebugId(props.variables, 'props.variables'),
)(theme.siteVariables)
: resolvedComponentVariables[componentKey]
}

// Fela plugins rely on `direction` param in `theme` prop instead of RTL
// Our API should be aligned with it
// Heads Up! Keep in sync with Design.tsx render logic
const direction = rtl ? 'rtl' : 'ltr'
const felaParam: RendererParam = {
theme: { direction },
disableAnimations,
displayName, // does not affect styles, only used by useEnhancedRenderer in docs
//
// STYLES
//

let resolveStylesResult: ResolveStylesResult

if (cacheEnabled && noInlineOverrides) {
const stylesKey = componentKey + JSON.stringify(restProps) + rtl + disableAnimations
let themeStylesCache = stylesCache.get(theme)

if (!themeStylesCache) {
themeStylesCache = {}
stylesCache.set(theme, themeStylesCache)
}

if (themeStylesCache[stylesKey]) {
resolveStylesResult = themeStylesCache[stylesKey]
} else {
resolveStylesResult = getResolvedStyles({
theme,
componentKey,
disableAnimations,
rtl,
renderer,
props,
resolvedVariables,
})

themeStylesCache[stylesKey] = resolveStylesResult
stylesCache.set(theme, themeStylesCache)
}
} else {
resolveStylesResult = getResolvedStyles({
theme,
componentKey,
disableAnimations,
rtl,
renderer,
props,
resolvedVariables,
})
}

const { resolvedStyles, resolvedStylesDebug, classes } = resolveStylesAndClasses(
mergedStyles,
styleParam,
(style: ICSSInJSStyle) => renderer.renderRule(() => style, felaParam),
)

classes.root = cx(className, classes.root, props.className)
const { classes, resolvedStylesDebug, resolvedStyles } = resolveStylesResult

// conditionally add sources for evaluating debug information to component
if (process.env.NODE_ENV !== 'production' && isDebugEnabled) {
saveDebug({
componentName: displayName,
componentVariables: _.filter(
// @ts-ignore
mnajdova marked this conversation as resolved.
Show resolved Hide resolved
resolvedVariables._debug,
variables => !_.isEmpty(variables.resolved),
),
Expand All @@ -139,6 +168,8 @@ const getStyles = (options: GetStylesOptions): GetStylesResult => {
})
}

classes.root = cx(componentClassName, classes.__root, className)

return {
classes,
variables: resolvedVariables,
Expand All @@ -147,4 +178,65 @@ const getStyles = (options: GetStylesOptions): GetStylesResult => {
}
}

const getResolvedStyles = ({
theme,
componentKey,
props,
resolvedVariables,
rtl,
disableAnimations,
renderer,
}: {
theme: ThemePrepared
componentKey: string
props: PropsWithVarsAndStyles & { design?: ComponentDesignProp }
resolvedVariables: object
rtl: boolean
disableAnimations: boolean
renderer: {
renderRule: RendererRenderRule
}
}): ResolveStylesResult => {
// Resolve styles using resolved variables, merge results, allow props.styles to override
let mergedStyles: ComponentSlotStylesPrepared = theme.componentStyles[componentKey] || {
root: () => ({}),
}

const hasInlineOverrides = !_.isNil(props.design) || !_.isNil(props.styles)

if (hasInlineOverrides) {
mergedStyles = mergeComponentStyles(
mergedStyles,
props.design && withDebugId({ root: props.design }, 'props.design'),
props.styles &&
withDebugId({ root: props.styles } as ComponentSlotStylesInput, 'props.styles'),
)
}

const styleParam: ComponentStyleFunctionParam = {
displayName: componentKey,
props,
variables: resolvedVariables,
theme,
rtl,
disableAnimations,
}

// Fela plugins rely on `direction` param in `theme` prop instead of RTL
// Our API should be aligned with it
// Heads Up! Keep in sync with Design.tsx render logic
const direction = rtl ? 'rtl' : 'ltr'
const felaParam: RendererParam = {
theme: { direction },
disableAnimations,
displayName: componentKey, // does not affect styles, only used by useEnhancedRenderer in docs
}

const result = resolveStylesAndClasses(mergedStyles, styleParam, (style: ICSSInJSStyle) =>
renderer.renderRule(() => style, felaParam),
)

return result
}

export default getStyles
28 changes: 17 additions & 11 deletions packages/react-bindings/src/styles/resolveStylesAndClasses.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import {
isDebugEnabled,
ICSSInJSStyle,
ComponentStyleFunctionParam,
ComponentSlotStylesResolved,
} from '@fluentui/styles'
import { ComponentSlotClasses } from '../styles/types'

export type ResolveStylesResult = {
resolvedStyles: ComponentSlotStylesResolved
resolvedStylesDebug: Record<string, { styles: Object }[]>
classes: ComponentSlotClasses
}

// Both resolvedStyles and classes are objects of getters with lazy evaluation
const resolveStylesAndClasses = (
mergedStyles: ComponentSlotStylesPrepared,
styleParam: ComponentStyleFunctionParam,
renderStyles: (styles: ICSSInJSStyle) => string,
): {
resolvedStyles: ICSSInJSStyle
resolvedStylesDebug: Record<string, { styles: Object }[]>
classes: ComponentSlotClasses
} => {
): ResolveStylesResult => {
const resolvedStyles: Record<string, ICSSInJSStyle> = {}
const resolvedStylesDebug: Record<string, { styles: Object }[]> = {}
const classes: Record<string, string> = {}
Expand Down Expand Up @@ -48,26 +51,29 @@ const resolveStylesAndClasses = (
},
})

Object.defineProperty(classes, slotName, {
const className = slotName === 'root' ? '__root' : slotName
const cacheClassKey = `${className}__return`

Object.defineProperty(classes, className, {
enumerable: false,
configurable: false,
set(val) {
classes[cacheKey] = val
classes[cacheClassKey] = val
return true
},
get() {
if (classes[cacheKey]) {
return classes[cacheKey]
if (classes[cacheClassKey]) {
return classes[cacheClassKey]
}

// this resolves the getter magic
const styleObj = resolvedStyles[slotName]

if (renderStyles && styleObj) {
classes[cacheKey] = renderStyles(styleObj)
classes[cacheClassKey] = renderStyles(styleObj)
}

return classes[cacheKey]
return classes[cacheClassKey]
},
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ describe('resolveStylesAndClasses', () => {
const renderStyles = jest.fn().mockReturnValue('a')
const { classes } = resolveStylesAndClasses(componentStyles, styleParam, renderStyles)

expect(classes.root).toBeDefined()
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(classes['__root']).toBeDefined()
expect(renderStyles).toHaveBeenCalledWith({ color: 'red' })
expect(classes.root).toBeDefined()
expect(classes['__root']).toBeDefined()
expect(renderStyles).toHaveBeenCalledTimes(1)
})
})
4 changes: 2 additions & 2 deletions packages/react/src/components/Chat/ChatItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from '../../utils'
import Box, { BoxProps } from '../Box/Box'

import { ComponentSlotStylesPrepared } from '@fluentui/styles'
import { ComponentSlotStylesResolved } from '@fluentui/styles'
import ChatMessage from './ChatMessage'

export interface ChatItemSlotClassNames {
Expand Down Expand Up @@ -86,7 +86,7 @@ class ChatItem extends UIComponent<WithAsProp<ChatItemProps>, any> {
)
}

renderChatItem(styles: ComponentSlotStylesPrepared) {
renderChatItem(styles: ComponentSlotStylesResolved) {
const { gutter, contentPosition } = this.props
const gutterElement =
gutter &&
Expand Down
Loading