diff --git a/apps/fluent-tester/macos/Podfile.lock b/apps/fluent-tester/macos/Podfile.lock index 384d5d5692..4f12ff3a39 100644 --- a/apps/fluent-tester/macos/Podfile.lock +++ b/apps/fluent-tester/macos/Podfile.lock @@ -3,10 +3,10 @@ PODS: - DoubleConversion (1.1.6) - FBLazyVector (0.74.30) - fmt (9.1.0) - - FRNAvatar (0.21.4): + - FRNAvatar (0.21.11): - MicrosoftFluentUI (= 0.13.1) - React - - FRNCallout (0.27.2): + - FRNCallout (0.27.9): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -28,13 +28,13 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - FRNCheckbox (0.17.9): + - FRNCheckbox (0.17.16): - React - - FRNMenuButton (0.13.19): + - FRNMenuButton (0.13.26): - React - - FRNRadioButton (0.21.16): + - FRNRadioButton (0.21.23): - React - - FRNVibrancyView (0.3.0): + - FRNVibrancyView (0.3.4): - React - glog (0.3.5) - MicrosoftFluentUI (0.13.1): @@ -123,7 +123,7 @@ PODS: - fmt (= 9.1.0) - glog - RCTDeprecation (0.74.30) - - RCTFocusZone (0.21.0): + - RCTFocusZone (0.21.7): - React - RCTRequired (0.74.30) - RCTTypeSafety (0.74.30): @@ -1262,11 +1262,11 @@ DEPENDENCIES: - FBLazyVector (from `../../../node_modules/react-native-macos/Libraries/FBLazyVector`) - fmt (from `../../../node_modules/react-native-macos/third-party-podspecs/fmt.podspec`) - FRNAvatar (from `../../../packages/experimental/Avatar/FRNAvatar.podspec`) - - "FRNCallout (from `../../../node_modules/@fluentui-react-native/callout`)" + - "FRNCallout (from `../node_modules/@fluentui-react-native/callout`)" - FRNCheckbox (from `../../../packages/experimental/Checkbox/FRNCheckbox.podspec`) - FRNMenuButton (from `../../../packages/components/MenuButton/FRNMenuButton.podspec`) - FRNRadioButton (from `../../../packages/components/RadioGroup/FRNRadioButton.podspec`) - - "FRNVibrancyView (from `../../../node_modules/@fluentui-react-native/vibrancy-view`)" + - "FRNVibrancyView (from `../node_modules/@fluentui-react-native/vibrancy-view`)" - glog (from `../../../node_modules/react-native-macos/third-party-podspecs/glog.podspec`) - RCT-Folly (from `../../../node_modules/react-native-macos/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../../../node_modules/react-native-macos/third-party-podspecs/RCT-Folly.podspec`) @@ -1341,7 +1341,7 @@ EXTERNAL SOURCES: FRNAvatar: :path: "../../../packages/experimental/Avatar/FRNAvatar.podspec" FRNCallout: - :path: "../../../node_modules/@fluentui-react-native/callout" + :path: "../node_modules/@fluentui-react-native/callout" FRNCheckbox: :path: "../../../packages/experimental/Checkbox/FRNCheckbox.podspec" FRNMenuButton: @@ -1349,7 +1349,7 @@ EXTERNAL SOURCES: FRNRadioButton: :path: "../../../packages/components/RadioGroup/FRNRadioButton.podspec" FRNVibrancyView: - :path: "../../../node_modules/@fluentui-react-native/vibrancy-view" + :path: "../node_modules/@fluentui-react-native/vibrancy-view" glog: :podspec: "../../../node_modules/react-native-macos/third-party-podspecs/glog.podspec" RCT-Folly: @@ -1466,17 +1466,17 @@ SPEC CHECKSUMS: DoubleConversion: 5b92c4507c560bb62e7aa1acdf2785ea3ff08b3b FBLazyVector: 0aa0591844f7fe4736f3aba70d30232edbd21eb5 fmt: 03574da4b7ba40de39da59677ca66610ce8c4a02 - FRNAvatar: ec4d219c71bfd3d74306a1fcf94f71393b63359f - FRNCallout: 1989375a3f3f704d7f85a560d979c222a5d2dc5c - FRNCheckbox: 80e3700277629ce802b1f07123e564f954d0e9b1 - FRNMenuButton: 43dd93252a1a6a0a3f4546b6d8021c2a572bf586 - FRNRadioButton: 8260e87a2df63c5a67ba23ce87dc8234ef137643 - FRNVibrancyView: 975f7e8ea14999015c3224743057418574833afa + FRNAvatar: 1eccbe629f3034e2caa11f62e16db120ac3c7836 + FRNCallout: 3eca65ff4ee29de3881a8402842b230626024ff6 + FRNCheckbox: 1d87e81b71e6706b6e94e69ab6923e50755c4a29 + FRNMenuButton: d87749093d3418d9c9377144f9e17e09af707335 + FRNRadioButton: 8b6c2d6c2d5c513ce493568ac0597d114359cb57 + FRNVibrancyView: 0fb5d289c2a4b934596fb26d81166151d66fa28c glog: ba31c1afa7dcf1915a109861bccdb4421be6175b MicrosoftFluentUI: dde98d8ed3fc306d9ddd0a6f0bc0c1f24fe5275e RCT-Folly: f47da9a444aae485a0528b3bccf0336156009d60 RCTDeprecation: 6c1d8fdaf3e34933c33a56531bd984bc2d22ef9e - RCTFocusZone: 50bf108af173c92cb8c4a776c17c37d010db31bc + RCTFocusZone: 999b4c2acb2193fd189f9ef6d1e970c2a6676250 RCTRequired: 5266165e3b6c7ca1554c5a75fb4c1ebe1bc60b53 RCTTypeSafety: ced894df76a17b8f7331d24e2efa862a7a616e89 React: 620dbf1e10232c8517a8b89d0def5b29e04ad24e diff --git a/change/@fluentui-react-native-framework-base-5fdded4e-da99-42ac-a660-501e4aa4e831.json b/change/@fluentui-react-native-framework-base-5fdded4e-da99-42ac-a660-501e4aa4e831.json new file mode 100644 index 0000000000..3604932430 --- /dev/null +++ b/change/@fluentui-react-native-framework-base-5fdded4e-da99-42ac-a660-501e4aa4e831.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "implement both new and old render patterns for classic and jsx-runtimes", + "packageName": "@fluentui-react-native/framework-base", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-native-immutable-merge-fe963967-3c5d-44ee-b7b6-bd67720856d6.json b/change/@fluentui-react-native-immutable-merge-fe963967-3c5d-44ee-b7b6-bd67720856d6.json new file mode 100644 index 0000000000..e3be8149aa --- /dev/null +++ b/change/@fluentui-react-native-immutable-merge-fe963967-3c5d-44ee-b7b6-bd67720856d6.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "update documentation", + "packageName": "@fluentui-react-native/immutable-merge", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-memo-cache-f82fa9b0-24fa-493e-90bf-23f4592908f3.json b/change/@fluentui-react-native-memo-cache-f82fa9b0-24fa-493e-90bf-23f4592908f3.json new file mode 100644 index 0000000000..07fc2a8fd7 --- /dev/null +++ b/change/@fluentui-react-native-memo-cache-f82fa9b0-24fa-493e-90bf-23f4592908f3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "update documentation", + "packageName": "@fluentui-react-native/memo-cache", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-merge-props-88bde9a7-67b6-40ac-9e3f-cf9e3e643597.json b/change/@fluentui-react-native-merge-props-88bde9a7-67b6-40ac-9e3f-cf9e3e643597.json new file mode 100644 index 0000000000..c245e723df --- /dev/null +++ b/change/@fluentui-react-native-merge-props-88bde9a7-67b6-40ac-9e3f-cf9e3e643597.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "update documentation", + "packageName": "@fluentui-react-native/merge-props", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-native-tester-e3900adc-3730-4ef8-8279-3c0f08e2c8b5.json b/change/@fluentui-react-native-tester-e3900adc-3730-4ef8-8279-3c0f08e2c8b5.json new file mode 100644 index 0000000000..44f52cd801 --- /dev/null +++ b/change/@fluentui-react-native-tester-e3900adc-3730-4ef8-8279-3c0f08e2c8b5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "rework jsx helpers for both new and old runtimes", + "packageName": "@fluentui-react-native/tester", + "email": "jasonmo@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/framework-base/README.md b/packages/framework-base/README.md index f29d99f7a6..6e3d33ed9e 100644 --- a/packages/framework-base/README.md +++ b/packages/framework-base/README.md @@ -1,20 +1,19 @@ -# Framework Base package +# `@fluentui-react-native/framework-base` -This package provides core types and helpers used by both the old and new versions of the framework. +This package provides core implementations and types to support both the legacy and current frameworks. Several previously standalone packages have had their implementations moved into this package. This allows them to share certain typings and helpers without having to work around circular dependency issues. The moved packages are: -- `@fluentui-react-native/immutable-merge` -- `@fluentui-react-native/memo-cache` -- `@fluentui-react-native/merge-props` +- [`@fluentui-react-native/immutable-merge`](./src/immutable-merge/README.md) +- [`@fluentui-react-native/memo-cache`](./src/memo-cache/README.md) +- [`@fluentui-react-native/merge-props`](./src/merge-props/README.md) -The functionality in these packages can be imported either by the base entry point for the package, or by using dedicated exports. The previous packages will continue to exist for the time being but are now just references to their individual exports. Note that export maps require special handling for metro bundling (with the exception of the jsx-runtime export) so the export maps are primarily for use -in JS/web projects. +The functionality in these packages is now exposed as part of this package. -## Type Helpers +## Component Patterns -- TODO: There are a number of issues with the way types are handled in the larger fluentui-react-native project, helpers and core types will be added here to help solve inference issues, avoid hard typecasts, and help the project eventually move to typescript 5.x. +The shared patterns for rendering components, as well as the JSX handlers have been centralized in this package. More information can be found [here](./src/component-patterns/README.md). -## JSX Helpers +## Type Helpers -- TODO: Both classic and the new jsx-runtime helpers will eventually come out of this package and be shared between old and new frameworks. This will be necessary to improve typing across the board. +- TODO: There are a number of issues with the way types are handled in the larger fluentui-react-native project, helpers and core types will be added here to help solve inference issues, avoid hard typecasts, and help the project eventually move to typescript 5.x. diff --git a/packages/framework-base/package.json b/packages/framework-base/package.json index 727cd8ec37..1fcbe1d958 100644 --- a/packages/framework-base/package.json +++ b/packages/framework-base/package.json @@ -16,6 +16,11 @@ "import": "./lib/index.js", "require": "./lib-commonjs/index.js", "types": "./lib/index.d.ts" + }, + "./jsx-runtime": { + "import": "./lib/jsx-runtime.js", + "require": "./lib-commonjs/jsx-runtime.js", + "types": "./lib/jsx-runtime.d.ts" } }, "scripts": { diff --git a/packages/framework-base/src/component-patterns/README.md b/packages/framework-base/src/component-patterns/README.md new file mode 100644 index 0000000000..18517b4ce8 --- /dev/null +++ b/packages/framework-base/src/component-patterns/README.md @@ -0,0 +1,39 @@ +# `fluentui-react-native` - Common component patterns + +These are the base component patterns shared across the deprecated or v0 framework (found under packages/deprecated), and the newer framework (found under packages/framework). This also includes the custom JSX handlers required to render them properly. + +There are two main patterns exposed here: direct rendering and staged rendering. + +## Direct Rendering + +The direct rendering pattern allows a component to be called directly, rather than creating a new entry in the DOM. + +As an example, if you want to create a wrapper around a component called `MyText` that has `italicize` as one of its props, that always wants to set that value to true. You could define: + +```ts +const MyNewText: React.FunctionComponent = (props) => { + return ; +} +``` + +When this is rendered, there is an entry for `MyNewText` which contains a `MyText` (another entry), which might contains `Text` (for react-native usage). The direct rendering pattern is one where a component can denote that it is safe to be called directly as a function, instead operating as a prop transform that gets applied to the underlying component. + +- For the above to be safe, `MyNewText` should NOT use hooks. In the case of any conditional rendering logic this will break the rule of hooks. + +There are two types of implementations in this folder: + +- `DirectComponent` - a functional component that marks itself as direct with a `_callDirect: true` attached property. This will then be called as a normal function component, with children included as part of props. +- `LegacyDirectComponent` - the pattern currently used in this library that should be moved away from. In this case `_canCompose: true` is set as an attached property, and the function component will be called with children split from props. + +The internal logic of the JSX rendering helpers will handle both patterns. In the case of the newer `DirectComponent` pattern, the component will still work, even without any jsx hooks, whereas the `LegacyDirectComponent` pattern will have a somewhat undefined behavior with regards to children. + +## Staged Rendering + +The issue with the direct component pattern above, is that hooks are integral to writing functional components. The staged rendering pattern is designed to help with this. In this case a component is implemented in two stages, the prep stage where hooks are called, and the rendering stage where the tree is emitted. + +As above there is a newer and older version of the pattern. + +- `StagedComponent` - the newer version of the pattern, where the returned component function expects children as part of props. +- `StagedRender` - the older version, where children are split out and JSX hooks are required to render correctly. + +Note that while the newer patterns work without any JSX hooks, the hooks will enable the element flattening. diff --git a/packages/framework-base/src/component-patterns/render.ts b/packages/framework-base/src/component-patterns/render.ts new file mode 100644 index 0000000000..80f0848f2e --- /dev/null +++ b/packages/framework-base/src/component-patterns/render.ts @@ -0,0 +1,54 @@ +import React from 'react'; +import * as ReactJSX from 'react/jsx-runtime'; +import type { RenderType, RenderResult, DirectComponent, LegacyDirectComponent } from './render.types'; + +export type CustomRender = () => RenderResult; + +function asDirectComponent(type: RenderType): DirectComponent | undefined { + if (typeof type === 'function' && (type as DirectComponent)._callDirect) { + return type as DirectComponent; + } + return undefined; +} + +function asLegacyDirectComponent(type: RenderType): LegacyDirectComponent | undefined { + if (typeof type === 'function' && (type as LegacyDirectComponent)._canCompose) { + return type as LegacyDirectComponent; + } + return undefined; +} + +export function renderForJsxRuntime( + type: React.ElementType, + props: React.PropsWithChildren, + key?: React.Key, + jsxFn: typeof ReactJSX.jsx = ReactJSX.jsx, +): RenderResult { + const legacyDirect = asLegacyDirectComponent(type); + if (legacyDirect) { + const { children, ...rest } = props; + const newProps = { ...rest, key }; + return legacyDirect(newProps, ...React.Children.toArray(children)) as RenderResult; + } + const directComponent = asDirectComponent(type); + if (directComponent) { + const newProps = { ...props, key }; + return directComponent(newProps); + } + return jsxFn(type, props, key); +} + +export function renderForClassicRuntime(type: RenderType, props: TProps, ...children: React.ReactNode[]): RenderResult { + const legacyDirect = asLegacyDirectComponent(type); + if (legacyDirect) { + return legacyDirect(props, ...children) as RenderResult; + } + const directComponent = asDirectComponent(type); + if (directComponent) { + const newProps = { ...props, children }; + return directComponent(newProps); + } + return React.createElement(type, props, ...children); +} + +export const renderSlot = renderForClassicRuntime; diff --git a/packages/framework-base/src/component-patterns/render.types.ts b/packages/framework-base/src/component-patterns/render.types.ts new file mode 100644 index 0000000000..52589e6a01 --- /dev/null +++ b/packages/framework-base/src/component-patterns/render.types.ts @@ -0,0 +1,114 @@ +import type React from 'react'; +import type ReactJSX from 'react/jsx-runtime'; + +/** + * Base types for rendering components in a react application, extracted from react types + */ +export type RenderResult = ReturnType; +export type RenderType = Parameters[0] | string; + +/** + * The standard element type inputs for react and react-native. This might be View or Button, or it might be 'div' in web. Effectively + * it is what react accepts for React.createElement + */ +export type NativeReactType = RenderType; + +/** + * DIRECT RENDERING + * + * This is a pattern where a function can by called directly to render a component, bypassing creating additional layers of the + * rendering tree. This is useful for higher order components that mainly need to do simple prop manipulation but want to + * compartmentalize the logic. + * + * Note that for this to be safe, hooks cannot be used in the function. This is the reason why function component is redefined, + * to help linting tools catch bad usage. + * + * The newer DirectComponent type should be used, as it will handle children consistently. + */ + +/** + * type of the render function, not a FunctionComponent to help prevent hook usage + */ +export type DirectComponentFunction = (props: TProps) => RenderResult; + +/** + * The full component definition that has the attached properties to allow the jsx handlers to render it directly. + */ +export type DirectComponent = DirectComponentFunction & { + displayName?: string; + _callDirect?: boolean; +}; + +/** + * Legacy slot function type, this allows the rendering handlers to bypass the normal JSX rendering and call the function + * directly. This expects the function to have children as the last argument of the call which isn't consistent with standard + * react usage, where children are passed as a prop. If writing new components use the DirectComponent type instead. + * @deprecated use DirectComponent instead + */ +export type LegacyDirectComponent = React.FunctionComponent & { + _canCompose?: boolean; +}; + +/** + * Legacy type name used for consistency with old rendering patterns. + */ +export type SlotFn = LegacyDirectComponent; + +/** + * MULTI-STAGE RENDERING + * + * The above direct rendering pattern is useful for simple components, but it does not allow for hooks or complex logic. The staged render pattern allows + * for a component to be rendered in two stages, allowing for hooks to be used in the first stage and then the second stage to be a simple render function that can + * be called directly. + * + * In code that respects the pattern the first stage will be called with props (though children will not be present) and will return a function that will be called + * with additional props, this time with children present. This allows for the first stage to handle all the logic and hooks, while the second stage can be a simple render function + * that can leverage direct rendering if supported. + * + * The component itself will be a FunctionComponent, but it will have an attached property that is the staged render function. This allows the component to be used in two + * parts via the useSlot hook, or to be used directly in JSX/TSX as a normal component. + */ + +/** + * This is an updated version of the staged render that handles children and types more consistently. Generally children + * will be passed as part of the props for component rendering, it is inconsistent to have them as a variable argument. + * + * The `children` prop will be automatically inferred and typed correctly by the prop type. Hooks are still expected + */ +export type TwoStageRender = (props: TProps) => React.ComponentType>; + +/** + * Component type for a component that can be rendered in two stages, with the attached render function. + */ +export type StagedComponent = React.FunctionComponent & { + _twoStageRender?: TwoStageRender; +}; + +/** + * The final rendering of the props in a staged render. This is the function component signature that matches that of + * React.createElement, children (if present) will be part of the variable args at the end. + */ +export type FinalRender = (props: TProps, ...children: React.ReactNode[]) => JSX.Element | null; + +/** + * Signature for a staged render function. + * @deprecated Use TwoStageRender instead + */ +export type StagedRender = (props: TProps, ...args: any[]) => FinalRender; + +/** + * Signature for a component that uses the staged render pattern. + * @deprecated Use TwoStageRender instead + */ +export type ComposableFunction = React.FunctionComponent & { _staged?: StagedRender }; + +/** + * A type aggregating all the custom types that can be used in the render process. + * @internal only used in this package, should not be exported + */ +export type AnyCustomType = + | React.FunctionComponent + | DirectComponent + | StagedComponent + | ComposableFunction + | LegacyDirectComponent; diff --git a/packages/framework-base/src/component-patterns/renderSlot.ts b/packages/framework-base/src/component-patterns/renderSlot.ts deleted file mode 100644 index ddf6e02a8a..0000000000 --- a/packages/framework-base/src/component-patterns/renderSlot.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; - -/** - * Component slots have a marker which allows the slot render handler to know which ones are safe to call as a function. - */ -export type SlotFn = React.FunctionComponent & { - _canCompose?: boolean; -}; - -/** - * The standard element type inputs for react and react-native. This might be View or Button, or it might be 'div' in web. Effectively - * it is what react accepts for React.createElement - */ -export type NativeReactType = React.ElementType | string; - -/** - * Renders a slot - * - * @param slot - native react type or slot function to render - * @param extraProps - additional props to mixin - * @param children - the children to pass down to the slot - */ -export function renderSlot(slot: NativeReactType | SlotFn, extraProps: TProps, ...children: React.ReactNode[]) { - return typeof slot === 'function' && (slot as SlotFn)._canCompose - ? (slot as SlotFn)(extraProps, ...children) - : React.createElement(slot, extraProps, ...children); -} diff --git a/packages/framework-base/src/component-patterns/stagedComponent.ts b/packages/framework-base/src/component-patterns/stagedComponent.ts deleted file mode 100644 index ab906146a7..0000000000 --- a/packages/framework-base/src/component-patterns/stagedComponent.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from 'react'; - -/** - * The final rendering of the props in a staged render. This is the function component signature that matches that of - * React.createElement, children (if present) will be part of the variable args at the end. - */ -export type FinalRender = (props: TProps, ...children: React.ReactNode[]) => JSX.Element | null; - -/** - * This is a pattern of rendering where a functional component can be executed in two stages rather than in a single pass. - * - * The pattern looks like: - * (props) => { - * // handle props - * // call hooks, remember these can't be conditional - * // build styles and props to pass to child components - * - * return (additionalProps, ...children) => { - * // return the actual element tree, this includes conditional branching or rendering - * // mixin additional props, props which require logic should be required in phase 1. - * - * // NOTE: This is where children will show up - * }; - * } - */ - -export type StagedRender = (props: TProps, ...args: any[]) => FinalRender; - -/** - * A composable function may have a two stage render function as an attached property. This allows the function to work - * in all the standard react flows, but allows for pulling out the staged render when components understand it. - */ -export type ComposableFunction = React.FunctionComponent & { _staged?: StagedRender }; - -function asArray(val: T | T[]): T[] { - return Array.isArray(val) ? val : [val]; -} - -/** - * Take a staged render function and make a real component out of it - * - * @param staged - staged render function to wrap into a staged component - * @param memo - optional flag to enable wrapping the created component in a React.memo HOC - */ -export function stagedComponent(staged: StagedRender, memo?: boolean): ComposableFunction { - const component = (props: React.PropsWithChildren) => { - const { children, ...rest } = props; - return staged(rest as TProps)({} as React.PropsWithChildren, asArray(children)); - }; - const stagedComponent = memo ? React.memo(component) : component; - Object.assign(stagedComponent, { _staged: staged }); - return stagedComponent as ComposableFunction; -} diff --git a/packages/framework-base/src/component-patterns/stagedComponent.tsx b/packages/framework-base/src/component-patterns/stagedComponent.tsx new file mode 100644 index 0000000000..492370ebe4 --- /dev/null +++ b/packages/framework-base/src/component-patterns/stagedComponent.tsx @@ -0,0 +1,45 @@ +/** + * @jsxRuntime classic + * @jsx withSlots + */ +import * as React from 'react'; +import { withSlots } from './withSlots'; + +import type { StagedComponent, TwoStageRender, StagedRender, ComposableFunction } from './render.types'; + +function asArray(val: T | T[]): T[] { + return Array.isArray(val) ? val : [val]; +} + +/** + * Take a staged render function and make a real component out of it + * + * @param staged - staged render function to wrap into a staged component + * @param memo - optional flag to enable wrapping the created component in a React.memo HOC + */ +export function stagedComponent(staged: StagedRender, memo?: boolean): ComposableFunction { + const component = (props: React.PropsWithChildren) => { + const { children, ...rest } = props; + return staged(rest as TProps)({} as React.PropsWithChildren, asArray(children)); + }; + const stagedComponent = memo ? React.memo(component) : component; + Object.assign(stagedComponent, { _staged: staged }); + return stagedComponent as ComposableFunction; +} + +/** + * Take a two stage render function and make a real component out of it, attaching the staged render function + * so it can be split if used in that manner. + * @param staged - two stage render function to wrap into a staged component + */ +export function twoStageComponent(staged: TwoStageRender): StagedComponent { + return Object.assign( + (props: React.PropsWithChildren) => { + const { children, ...outerProps } = props; + const innerProps = { children } as React.PropsWithChildren; + const Inner = staged(outerProps as TProps); + return ; + }, + { _twoStageRender: staged }, + ); +} diff --git a/packages/framework-base/src/component-patterns/withSlots.tsx b/packages/framework-base/src/component-patterns/withSlots.tsx index 5b52433f5a..a9b1c137ec 100644 --- a/packages/framework-base/src/component-patterns/withSlots.tsx +++ b/packages/framework-base/src/component-patterns/withSlots.tsx @@ -1,5 +1,5 @@ -import type { NativeReactType } from './renderSlot'; -import { renderSlot } from './renderSlot'; +import type React from 'react'; +import { renderForClassicRuntime } from './render'; /** * This function is required for any module that uses slots. @@ -15,11 +15,7 @@ import { renderSlot } from './renderSlot'; */ // Can't use typeof on React.createElement since it's overloaded. Approximate createElement's signature for now and widen as needed. -export function withSlots

( - reactType: NativeReactType, - props?: (React.Attributes & P) | null, - ...children: React.ReactNode[] -): ReturnType> { +export function withSlots(reactType: Parameters[0], props?: unknown, ...children: React.ReactNode[]) { // if it is a non-string type with _canCompose set just call the function directly, otherwise call createElement as normal - return renderSlot

(reactType, props, ...children); + return renderForClassicRuntime(reactType, props, ...children); } diff --git a/packages/framework-base/src/immutable-merge/README.md b/packages/framework-base/src/immutable-merge/README.md new file mode 100644 index 0000000000..6af65b94a1 --- /dev/null +++ b/packages/framework-base/src/immutable-merge/README.md @@ -0,0 +1,185 @@ +# Immutable Merge package + +This package provides a relatively concise routine to handle merging multiple objects together with the following characteristics: + +- No modifications will be made to any object +- Minimal updates. If only one value is updated three levels deep, only that value and the chain of containing objects will be recreated. +- Empty objects or undefined objects will be ignored and not cause a new branch to be created. +- Recursion is controllable in a variety of ways + +Note that this does not provide a **strict** immutable package on its own. It also doesn't operate or return only readonly objects. This is to provide flexibility. It could easily be wrapped in various ways to provide that type of functionality, but it is provided in a more flexible form to be useful in other scenarios as well. + +## Deep Merge via `immutableMerge` + +For standard deep merging, this package provides the `immutableMerge` function. The signature is as follows: + +```ts +export function immutableMerge(...objs: (T | undefined)[]): T | undefined; +``` + +This takes one or more objects of type `T` and deep merges them. If objects are undefined or null in some manner they will be ignored. Merging via this routine (and all routines in the package) typically follow the semantics of `Object.assign`, with a few extra behaviors. + +- all values at a given level will overwrite, with the last writer winning +- if a key does not exist for an object it is ignored +- if a key does exist, even if it is `undefined` it will replace the previous value +- only non-array objects will recurse, arrays will be replaced rather than appended +- keys which exist and have a value of `undefined` will be deleted + +The peculiar pattern of deleting keys which end up as undefined is the only way to delete a key without violating the core principles. An example of key deletion might look like: + +```ts +const newObj = immutableMerge(myObj, { keyToDelete1: undefined, keyToDelete2: undefined }); +``` + +## Custom Merging via `immutableMergeCore` + +In many cases, merges have to follow additional rules to match the structure or behavior of objects passed in. This results in authoring custom merge routines to handle this constraint. This package allows for deep customization of merge behaviors via `immutableMergeCore`. + +```ts +export function immutableMergeCore(options: RecursionOptions | MergeOptions, ...objs: (T | undefined)[]): T | undefined; +``` + +### RecursionOptions + +Recursion options can be a boolean or a number with behavior interpreted as follows: + +- `boolean` - Should this recurse. If the value is `true` it will recurse infinitely, if `false` it will not recurse. +- `number` - Recursion depth. A value of `0` will not recurse any farther, a positive value will recurse that many additional levels before stopping, a negative value will recurse indefinitely. + +### MergeOptions + +This object allows very precise control of the recursion. At a given level it matches values by name of the key, or by the resulting type of the property. + +```ts +export interface MergeOptions { + [typeOrName: string]: RecursionOptions | RecursionHandlers | MergeOptions; +} +``` + +Matching will happen in the following order: + +1. Merged object property key matches a key in MergeOptions. +2. The type of the key is referenced in MergeOptions. Note that arrays (which are objects) are treated as being of type 'array' for this purpose. + +The values within the options can have the following types: + +| Type | Usage | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `RecursionOption` | Behaves as if this value was passed into the recursive call. So 0/false mean merge but don't recurse for the matching child object, less than zero / true means recurse deeply, greater than zero means recurse that many times. | +| `RecursionHandler` | Run a function to handle the merge or invoke one of the built-in handlers for the library. See below for more information. | +| `MergeOptions` | Forward the child `MergeOptions` to the child level when making the recursive call. | + +### RecursionHandler + +When merging values for a given key, providing a recursion handler allows custom processing. A handler function has the following signature: + +```ts +export type CustomRecursionHandler = (...vals: any[]) => any; +``` + +The vals parameter will have collected all the non-undefined values from the input objects. Note that type checking is the responsibility of the handler function. + +#### Built in handlers + +Built-in handlers can be referenced by name. The currently supported built-in handlers are as follows: + +| Handler | Description | +| ------------- | ------------------------------------------- | +| `appendArray` | Append arrays rather than overwriting them. | + +### Example Usage + +As an example, imagine props for react components, with a concept called SlotProps that has multiple props in the same object. + +```ts +export interface IStandardProps { + classNames?: string; + tokens?: ISomeObjectTypeWithoutStyle; + style?: CSSVariables | CSSVariables[]; + // likely more +} + +export interface IMyComponentProps extends IStandardProps { + // other stuff here +} + +export interface IComponentSlotProps { + root: IMyComponentProps; + slot1: ISomeOtherComponentProps; + slot2: IYetAnotherComponentProps; +} +``` + +In this case style needs to be merged in a special manner and classNames need to be appended. Deep recursion is not desireable in the case where a prop might be an object as with partial values you might get unexpected behavior. Here are some examples of ways to make merge routines: + +```ts +// all in one function +export function mergeSlotProps1(...slotProps: ISlotPropsBase[]): ISlotPropsBase { + return immutableMergeCore({ + // match any object + object: { + // match tokens and merge but don't recurse + tokens: 0, + // run the string merge routine on classNames + classNames: (...names: string[]) => { names.map(name => name.trim()).join(' ') }, + // run an existing style merge routine on styles + style: mergeStyles; + } + }, ...slotProps); +} + +// this could be broken into two parts, options for props +const propsOptions: MergeOptions = { + tokens: 0, + classNames: (...names: string[]) => { names.map(name => name.trim()).join(' ') }, + style: mergeStyles; +} + +// then options for slotProps that refer to the props object +const slotPropsOptions: MergeOptions = { + object: propsOptions; +} + +// then a wrapper for each +export function mergeProps(...props: T[]): T { + return immutableMergeCore(propsOptions, ...props); +} + +export function mergeSlotProps(...slotProps: T[]): T { + return immutableMergeCore(slotPropsOptions, ...slotProps); +} + +``` + +## processImmutable + +The ability to run a handler on something like a style as a part of merge is useful but in normal usage it has some limitations. If there is only one object in a branch or only one value of that type the handler won't run. If the processors are essential functions, or if it is desireable to run processors on a single object you can use `processImmutable`. + +```ts +export function processImmutable(options: MergeOptions, ...objs: (T | undefined)[]): T | undefined; +``` + +This convenience function runs the merge routine as a processor for one or more objects. An example use case might be to turn all style entries into a css class name if it is not already a css class name. This should have the following behavior: + +- Style values two levels down should be processed +- The object should remain unchanged if nothing changed +- branches which are unchanged should be untouched +- If a style gets updated the object should be mimally mutated + +The usage would be as follows. Given a processor called `myStyleProcessor`: + +```ts +let complexObject: IMyObjtype = getObjectFromSomewhere(); +complexObject = processImmutable( + { + object: { + object: { + style: myStyleProcessor, + }, + }, + }, + complexObject, +); +``` + +While the primary use case is for a single object this allows merging to happen at the same time if so desired. Merging happens as normal with the exception that processors will still be called in the case where there is only one object. diff --git a/packages/framework-base/src/index.ts b/packages/framework-base/src/index.ts index ca06f9d94c..3a2fd0510d 100644 --- a/packages/framework-base/src/index.ts +++ b/packages/framework-base/src/index.ts @@ -20,8 +20,21 @@ export { mergeStyles } from './merge-props/mergeStyles'; export { mergeProps } from './merge-props/mergeProps'; // component pattern exports -export { renderSlot } from './component-patterns/renderSlot'; -export type { SlotFn, NativeReactType } from './component-patterns/renderSlot'; +export { renderForClassicRuntime, renderForJsxRuntime, renderSlot } from './component-patterns/render'; +export type { + DirectComponent, + DirectComponentFunction, + LegacyDirectComponent, + StagedComponent, + StagedRender, + TwoStageRender, + RenderType, + RenderResult, + ComposableFunction, + FinalRender, + SlotFn, + NativeReactType, +} from './component-patterns/render.types'; export { withSlots } from './component-patterns/withSlots'; -export { stagedComponent } from './component-patterns/stagedComponent'; -export type { FinalRender, StagedRender, ComposableFunction } from './component-patterns/stagedComponent'; +export { stagedComponent, twoStageComponent } from './component-patterns/stagedComponent'; +export { jsx, jsxs } from './jsx-runtime'; diff --git a/packages/framework-base/src/jsx-runtime.ts b/packages/framework-base/src/jsx-runtime.ts new file mode 100644 index 0000000000..b5f19bddf7 --- /dev/null +++ b/packages/framework-base/src/jsx-runtime.ts @@ -0,0 +1,11 @@ +import type React from 'react'; +import * as ReactJSX from 'react/jsx-runtime'; +import { renderForJsxRuntime } from './component-patterns/render'; + +export function jsx(type: React.ElementType, props: React.PropsWithChildren, key?: React.Key): React.ReactElement { + return renderForJsxRuntime(type, props, key, ReactJSX.jsx); +} + +export function jsxs(type: React.ElementType, props: React.PropsWithChildren, key?: React.Key): React.ReactElement { + return renderForJsxRuntime(type, props, key, ReactJSX.jsxs); +} diff --git a/packages/framework-base/src/memo-cache/README.md b/packages/framework-base/src/memo-cache/README.md new file mode 100644 index 0000000000..0a2b727aac --- /dev/null +++ b/packages/framework-base/src/memo-cache/README.md @@ -0,0 +1,141 @@ +# @fluentui-react-native/memo-cache + +This package implements a hierarchical memoization cache using an API pattern that mimics the react.js useMemo hook. It also provides an implementation of traditional JavaScript memoization built using react style utility. + +Memoization is an optimization pattern used when a discrete set of inputs, typically parameters to an expensive function, yield a deterministic output. In this case, if the inputs match a previous call, a cached result can be retrieved. This is typically implemented as a factory function, which wraps a function in a closure, adding implicit caching. + +React.js provides a hook called `useMemo` which is shifts to a more explicit model, where the keys are listed explicitly. This allows more control over the inputs than the older pattern. It's worth mentioning that the react `useMemo` hook is not a global cache, it is attached to a given component instance and compares the current execution with the previous one. + +### When to Use This + +This package can be beneficial in two scenarios: + +#### Performance + +If the routine to be memoized is expensive, then caching the results can boost performance. Note that cache lookups have cost themselves so memoizing a trivial function will likely be slower. + +Also note that every additional key adds a level of depth to the hierarchical cache. This has expense and reduces the likelihood of data being already in the cache. Collapsing the inputs to a manageable set helps optimize this. For instance, if building a style from a theme definition pulls 8 values from a theme, it is more efficient to key the resulting object on the theme, than to key each property individually. + +#### Object Identity + +The other benefit to this pattern is maintaining object identity between subsequent calls. In react-native the object identity of the style property will sometimes be compared, even if the values within are identical, the shallow props compare will not see the objects as the same. + +Similarly if a style is being turned into a CSS class (which is expensive), a `WeakMap` to map style objects to CSS classes will only work if the object identities are maintained. + +## Usage guide + +The baseline cache pattern is defined by the following type: + +```ts +export type GetMemoValue = ( + factory: T | () => T, + keys: any[] +) => [T, GetMemoValue]; +``` + +The parameters are used as follows: + +| Param | Description | +| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `factory` | This is typically a function, often a simple closure, which returns a value. This value will be cached for a given set of keys. Subsequent calls will just return the value without executing the function. This can also be a value type in which case that will be returned directly. | +| `keys` | Variable parameter list, used as the keys for caching. Note that the order of keys matter. [A, B] resolves differently than [B, A]. | + +The return result is a tuple with two elements containing: + +- The result of the factory function, or the value of factory if it is not a function +- A function for caching which is local to the previous keys queried + +This recursive calling pattern allows for a natural pipelining of caching and memoization. Because the cache is effectively implemented as a tree, this pattern falls out fairly easily. See the examples below for more detail. + +### getMemoCache + +To get an instance of the memo cache to work with, a caller starts by calling `getMemoCache`. + +```ts +export function getMemoCache(globalKey?: object): GetMemoValue; +``` + +This function takes an optional parameter `globalKey` which can be an object to use as a base cache reference. + +- If `globalKey` is specified, the same cache will be retrieved from the global call with the same object reference. +- If `globalKey` is not specified the cache instance will be unique and contained entirely within the returned function. + +### memoize + +This library also provides a standard memoization wrapper: + +```ts +export function memoize(fn: T): T; +``` + +This should be able to handle any function as an input. It will create its own instance of the cache, use any parameters to the function as key values, and return a closure with the same signature as the input. + +This should support the following: + +- Function with any number of parameters +- Functions with no parameters +- Any return result +- Void functions + +## Examples + +The following are some examples for how to use the functions above for various optimizations. + +### Merge styles to ensure object identity does not change + +Given a function to merge styles together, wrap it in a memoization helper to ensure object identities don't change. Then add a helper for ensuring a CSS rule exists for a set of styles. + +```ts +// standard function which will be memoized +function mergeStylesWorker(...cssStyles: CSSStyle[]): CSSStyle { + // do the work of merging multiple styles together to form a new CSS style +} + +// exported function internally has a caching layer with memoize +export const mergeStyles = memoize(mergeStylesWorker); + +// all-in-one authoring of memoized function, this one to turn a style into a CSS class, traditionally +// an expensive operation in browsers +export const createRuleForStyle = memoize((style: CSSStyle) => { + const className = // do the work of creating the rule + return className; +}); +``` + +### Hierarchical Theme Caching + +This demonstrates a component called `MyComponent` that: + +1. has a unique cache based on the component identity +2. cached a style computed against a theme +3. optionally merges a style from props and caches that result + +These three levels of caching are effectively instance -> theme -> props.style. + +```ts +import { getMemoCache } from '@fluentui-react-native/memo-cache'; + +// get a unique cache for this component +const myComponentCache = getMemoCache(); + +// component is a function that takes props +export const MyComponent = (props: IMyComponentProps) => { + const theme = useContext(ThemeContext); + const newProps = { ...props }; + + // get the style, cached against the theme so it will only be called once, note that because + const [style, themeLocalCache] = myComponentCache(() => { + const colors = theme.colors; + return { + backgroundColor: colors.neutralBackground, + color: colors.neutralForeground, + // more stuff from theme + }; + }, [theme]); + + // merge the styles if a style is passed in via props, caching the union to ensure consistent object identity + newProps.style = newProps.style ? themeLocalCache(() => mergeStyles(style, newProps.style), [newProps.style])[0] : style; + + return ; +}; +``` diff --git a/packages/framework-base/src/merge-props/README.md b/packages/framework-base/src/merge-props/README.md new file mode 100644 index 0000000000..1ed1b15b8c --- /dev/null +++ b/packages/framework-base/src/merge-props/README.md @@ -0,0 +1,43 @@ +# @fluentui-react-native/merge-props + +Utilities for merging styles and props (which contain styles) + +## Merging Props + +The `mergeProps` routine handles merging props together. Generally this is a standard per property merge identical to the behavior of `Object.assign` with the following two exceptions: + +- Objects under `props.style` will be merged using `mergeStyle` above, including caching the resolved styles +- Strings contained in `props.className` will be joined together using spaces as a delimiter. + +## Merging Styles + +Styles are defined using the standard react-native pattern and will be merged in a way that maintains object identity where possible. + +### StyleProp + +This is a copy of the StyleProp definition from `react-native`. This is copied primarily in the case where it is used in web code where adding a dependency on the `react-native` package itself is not desireable. + +The StyleProp pattern itself is allows a style to be provided as a style or a recursive array of styles. So the following pattern is allowed: + +```ts +props = { + style: [{ ...style1 }, [{ ...style2 }, { ...style3 }, [{ ...style4 }]], { ...style5 }], +}; +``` + +In this model merging styles can be effectively deferred by the following: + +```ts +const styleToMerge = { ...values }; +props.style = [props.style, styleToMerge]; +``` + +### mergeStyles + +This routine merges one or more react-native styles together. The inputs are styles in the `StyleProp` format referenced above. The various input styles will be flattened and merged together to produce a single non-flattened output style. + +```ts +function mergeStyles(...styles: StyleProp[]): T; +``` + +This routine has a built-in caching layer that will attempt to ensure that object identity remains consistent. This means that style A + style B, where the references to A and B are the same, will always produce object C, where the reference will also be the same. diff --git a/packages/framework/immutable-merge/README.md b/packages/framework/immutable-merge/README.md index 2a76371b8c..a9968552af 100644 --- a/packages/framework/immutable-merge/README.md +++ b/packages/framework/immutable-merge/README.md @@ -1,185 +1,5 @@ -# Immutable Merge package +# `@fluentui-react-native/immutable-merge` -This package provides a relatively concise routine to handle merging multiple objects together with the following characteristics: +This package is now a pass-through package which references `@fluentui-react-native/framework-base` for backwards compatibility. -- No modifications will be made to any object -- Minimal updates. If only one value is updated three levels deep, only that value and the chain of containing objects will be recreated. -- Empty objects or undefined objects will be ignored and not cause a new branch to be created. -- Recursion is controllable in a variety of ways - -Note that this does not provide a **strict** immutable package on its own. It also doesn't operate or return only readonly objects. This is to provide flexibility. It could easily be wrapped in various ways to provide that type of functionality, but it is provided in a more flexible form to be useful in other scenarios as well. - -## Deep Merge via `immutableMerge` - -For standard deep merging, this package provides the `immutableMerge` function. The signature is as follows: - -```ts -export function immutableMerge(...objs: (T | undefined)[]): T | undefined; -``` - -This takes one or more objects of type `T` and deep merges them. If objects are undefined or null in some manner they will be ignored. Merging via this routine (and all routines in the package) typically follow the semantics of `Object.assign`, with a few extra behaviors. - -- all values at a given level will overwrite, with the last writer winning -- if a key does not exist for an object it is ignored -- if a key does exist, even if it is `undefined` it will replace the previous value -- only non-array objects will recurse, arrays will be replaced rather than appended -- keys which exist and have a value of `undefined` will be deleted - -The peculiar pattern of deleting keys which end up as undefined is the only way to delete a key without violating the core principles. An example of key deletion might look like: - -```ts -const newObj = immutableMerge(myObj, { keyToDelete1: undefined, keyToDelete2: undefined }); -``` - -## Custom Merging via `immutableMergeCore` - -In many cases, merges have to follow additional rules to match the structure or behavior of objects passed in. This results in authoring custom merge routines to handle this constraint. This package allows for deep customization of merge behaviors via `immutableMergeCore`. - -```ts -export function immutableMergeCore(options: RecursionOptions | MergeOptions, ...objs: (T | undefined)[]): T | undefined; -``` - -### RecursionOptions - -Recursion options can be a boolean or a number with behavior interpreted as follows: - -- `boolean` - Should this recurse. If the value is `true` it will recurse infinitely, if `false` it will not recurse. -- `number` - Recursion depth. A value of `0` will not recurse any farther, a positive value will recurse that many additional levels before stopping, a negative value will recurse indefinitely. - -### MergeOptions - -This object allows very precise control of the recursion. At a given level it matches values by name of the key, or by the resulting type of the property. - -```ts -export interface MergeOptions { - [typeOrName: string]: RecursionOptions | RecursionHandlers | MergeOptions; -} -``` - -Matching will happen in the following order: - -1. Merged object property key matches a key in MergeOptions. -2. The type of the key is referenced in MergeOptions. Note that arrays (which are objects) are treated as being of type 'array' for this purpose. - -The values within the options can have the following types: - -| Type | Usage | -| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `RecursionOption` | Behaves as if this value was passed into the recursive call. So 0/false mean merge but don't recurse for the matching child object, less than zero / true means recurse deeply, greater than zero means recurse that many times. | -| `RecursionHandler` | Run a function to handle the merge or invoke one of the built-in handlers for the library. See below for more information. | -| `MergeOptions` | Forward the child `MergeOptions` to the child level when making the recursive call. | - -### RecursionHandler - -When merging values for a given key, providing a recursion handler allows custom processing. A handler function has the following signature: - -```ts -export type CustomRecursionHandler = (...vals: any[]) => any; -``` - -The vals parameter will have collected all the non-undefined values from the input objects. Note that type checking is the responsibility of the handler function. - -#### Built in handlers - -Built-in handlers can be referenced by name. The currently supported built-in handlers are as follows: - -| Handler | Description | -| ------------- | ------------------------------------------- | -| `appendArray` | Append arrays rather than overwriting them. | - -### Example Usage - -As an example, imagine props for react components, with a concept called SlotProps that has multiple props in the same object. - -```ts -export interface IStandardProps { - classNames?: string; - tokens?: ISomeObjectTypeWithoutStyle; - style?: CSSVariables | CSSVariables[]; - // likely more -} - -export interface IMyComponentProps extends IStandardProps { - // other stuff here -} - -export interface IComponentSlotProps { - root: IMyComponentProps; - slot1: ISomeOtherComponentProps; - slot2: IYetAnotherComponentProps; -} -``` - -In this case style needs to be merged in a special manner and classNames need to be appended. Deep recursion is not desireable in the case where a prop might be an object as with partial values you might get unexpected behavior. Here are some examples of ways to make merge routines: - -```ts -// all in one function -export function mergeSlotProps1(...slotProps: ISlotPropsBase[]): ISlotPropsBase { - return immutableMergeCore({ - // match any object - object: { - // match tokens and merge but don't recurse - tokens: 0, - // run the string merge routine on classNames - classNames: (...names: string[]) => { names.map(name => name.trim()).join(' ') }, - // run an existing style merge routine on styles - style: mergeStyles; - } - }, ...slotProps); -} - -// this could be broken into two parts, options for props -const propsOptions: MergeOptions = { - tokens: 0, - classNames: (...names: string[]) => { names.map(name => name.trim()).join(' ') }, - style: mergeStyles; -} - -// then options for slotProps that refer to the props object -const slotPropsOptions: MergeOptions = { - object: propsOptions; -} - -// then a wrapper for each -export function mergeProps(...props: T[]): T { - return immutableMergeCore(propsOptions, ...props); -} - -export function mergeSlotProps(...slotProps: T[]): T { - return immutableMergeCore(slotPropsOptions, ...slotProps); -} - -``` - -## processImmutable - -The ability to run a handler on something like a style as a part of merge is useful but in normal usage it has some limitations. If there is only one object in a branch or only one value of that type the handler won't run. If the processors are essential functions, or if it is desireable to run processors on a single object you can use `processImmutable`. - -```ts -export function processImmutable(options: MergeOptions, ...objs: (T | undefined)[]): T | undefined; -``` - -This convenience function runs the merge routine as a processor for one or more objects. An example use case might be to turn all style entries into a css class name if it is not already a css class name. This should have the following behavior: - -- Style values two levels down should be processed -- The object should remain unchanged if nothing changed -- branches which are unchanged should be untouched -- If a style gets updated the object should be mimally mutated - -The usage would be as follows. Given a processor called `myStyleProcessor`: - -```ts -let complexObject: IMyObjtype = getObjectFromSomewhere(); -complexObject = processImmutable( - { - object: { - object: { - style: myStyleProcessor - } - } - }, - complexObject -); -``` - -While the primary use case is for a single object this allows merging to happen at the same time if so desired. Merging happens as normal with the exception that processors will still be called in the case where there is only one object. +The current documentation can be found [here](../../framework-base/src/immutable-merge/README.md). diff --git a/packages/framework/memo-cache/README.md b/packages/framework/memo-cache/README.md index f0ba116f73..bd8f4758e8 100644 --- a/packages/framework/memo-cache/README.md +++ b/packages/framework/memo-cache/README.md @@ -1,141 +1,5 @@ -# @fluentui-react-native/memo-cache +# `@fluentui-react-native/memo-cache` -This package implements a hierarchical memoization cache using an API pattern that mimics the react.js useMemo hook. It also provides an implementation of traditional JavaScript memoization built using react style utility. +This package is now a pass-through package which references `@fluentui-react-native/framework-base` for backwards compatibility. -Memoization is an optimization pattern used when a discrete set of inputs, typically parameters to an expensive function, yield a deterministic output. In this case, if the inputs match a previous call, a cached result can be retrieved. This is typically implemented as a factory function, which wraps a function in a closure, adding implicit caching. - -React.js provides a hook called `useMemo` which is shifts to a more explicit model, where the keys are listed explicitly. This allows more control over the inputs than the older pattern. It's worth mentioning that the react `useMemo` hook is not a global cache, it is attached to a given component instance and compares the current execution with the previous one. - -### When to Use This - -This package can be beneficial in two scenarios: - -#### Performance - -If the routine to be memoized is expensive, then caching the results can boost performance. Note that cache lookups have cost themselves so memoizing a trivial function will likely be slower. - -Also note that every additional key adds a level of depth to the hierarchical cache. This has expense and reduces the likelihood of data being already in the cache. Collapsing the inputs to a manageable set helps optimize this. For instance, if building a style from a theme definition pulls 8 values from a theme, it is more efficient to key the resulting object on the theme, than to key each property individually. - -#### Object Identity - -The other benefit to this pattern is maintaining object identity between subsequent calls. In react-native the object identity of the style property will sometimes be compared, even if the values within are identical, the shallow props compare will not see the objects as the same. - -Similarly if a style is being turned into a CSS class (which is expensive), a `WeakMap` to map style objects to CSS classes will only work if the object identities are maintained. - -## Usage guide - -The baseline cache pattern is defined by the following type: - -```ts -export type GetMemoValue = ( - factory: T | () => T, - keys: any[] -) => [T, GetMemoValue]; -``` - -The parameters are used as follows: - -| Param | Description | -| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `factory` | This is typically a function, often a simple closure, which returns a value. This value will be cached for a given set of keys. Subsequent calls will just return the value without executing the function. This can also be a value type in which case that will be returned directly. | -| `keys` | Variable parameter list, used as the keys for caching. Note that the order of keys matter. [A, B] resolves differently than [B, A]. | - -The return result is a tuple with two elements containing: - -- The result of the factory function, or the value of factory if it is not a function -- A function for caching which is local to the previous keys queried - -This recursive calling pattern allows for a natural pipelining of caching and memoization. Because the cache is effectively implemented as a tree, this pattern falls out fairly easily. See the examples below for more detail. - -### getMemoCache - -To get an instance of the memo cache to work with, a caller starts by calling `getMemoCache`. - -```ts -export function getMemoCache(globalKey?: object): GetMemoValue; -``` - -This function takes an optional parameter `globalKey` which can be an object to use as a base cache reference. - -- If `globalKey` is specified, the same cache will be retrieved from the global call with the same object reference. -- If `globalKey` is not specified the cache instance will be unique and contained entirely within the returned function. - -### memoize - -This library also provides a standard memoization wrapper: - -```ts -export function memoize(fn: T): T; -``` - -This should be able to handle any function as an input. It will create its own instance of the cache, use any parameters to the function as key values, and return a closure with the same signature as the input. - -This should support the following: - -- Function with any number of parameters -- Functions with no parameters -- Any return result -- Void functions - -## Examples - -The following are some examples for how to use the functions above for various optimizations. - -### Merge styles to ensure object identity does not change - -Given a function to merge styles together, wrap it in a memoization helper to ensure object identities don't change. Then add a helper for ensuring a CSS rule exists for a set of styles. - -```ts -// standard function which will be memoized -function mergeStylesWorker(...cssStyles: CSSStyle[]): CSSStyle { - // do the work of merging multiple styles together to form a new CSS style -} - -// exported function internally has a caching layer with memoize -export const mergeStyles = memoize(mergeStylesWorker); - -// all-in-one authoring of memoized function, this one to turn a style into a CSS class, traditionally -// an expensive operation in browsers -export const createRuleForStyle = memoize((style: CSSStyle) => { - const className = // do the work of creating the rule - return className; -}); -``` - -### Hierarchical Theme Caching - -This demonstrates a component called `MyComponent` that: - -1. has a unique cache based on the component identity -2. cached a style computed against a theme -3. optionally merges a style from props and caches that result - -These three levels of caching are effectively instance -> theme -> props.style. - -```ts -import { getMemoCache } from '@fluentui-react-native/memo-cache'; - -// get a unique cache for this component -const myComponentCache = getMemoCache(); - -// component is a function that takes props -export const MyComponent = (props: IMyComponentProps) => { - const theme = useContext(ThemeContext); - const newProps = { ...props }; - - // get the style, cached against the theme so it will only be called once, note that because - const [style, themeLocalCache] = myComponentCache(() => { - const colors = theme.colors; - return { - backgroundColor: colors.neutralBackground, - color: colors.neutralForeground - // more stuff from theme - }; - }, [theme]); - - // merge the styles if a style is passed in via props, caching the union to ensure consistent object identity - newProps.style = newProps.style ? themeLocalCache(() => mergeStyles(style, newProps.style), [newProps.style])[0] : style; - - return ; -}; -``` +The current documentation can be found [here](../../framework-base/src/memo-cache/README.md). diff --git a/packages/framework/merge-props/README.md b/packages/framework/merge-props/README.md index 1ed1b15b8c..2817ce85a4 100644 --- a/packages/framework/merge-props/README.md +++ b/packages/framework/merge-props/README.md @@ -1,43 +1,5 @@ -# @fluentui-react-native/merge-props +# `@fluentui-react-native/merge-props` -Utilities for merging styles and props (which contain styles) +This package is now a pass-through package which references `@fluentui-react-native/framework-base` for backwards compatibility. -## Merging Props - -The `mergeProps` routine handles merging props together. Generally this is a standard per property merge identical to the behavior of `Object.assign` with the following two exceptions: - -- Objects under `props.style` will be merged using `mergeStyle` above, including caching the resolved styles -- Strings contained in `props.className` will be joined together using spaces as a delimiter. - -## Merging Styles - -Styles are defined using the standard react-native pattern and will be merged in a way that maintains object identity where possible. - -### StyleProp - -This is a copy of the StyleProp definition from `react-native`. This is copied primarily in the case where it is used in web code where adding a dependency on the `react-native` package itself is not desireable. - -The StyleProp pattern itself is allows a style to be provided as a style or a recursive array of styles. So the following pattern is allowed: - -```ts -props = { - style: [{ ...style1 }, [{ ...style2 }, { ...style3 }, [{ ...style4 }]], { ...style5 }], -}; -``` - -In this model merging styles can be effectively deferred by the following: - -```ts -const styleToMerge = { ...values }; -props.style = [props.style, styleToMerge]; -``` - -### mergeStyles - -This routine merges one or more react-native styles together. The inputs are styles in the `StyleProp` format referenced above. The various input styles will be flattened and merged together to produce a single non-flattened output style. - -```ts -function mergeStyles(...styles: StyleProp[]): T; -``` - -This routine has a built-in caching layer that will attempt to ensure that object identity remains consistent. This means that style A + style B, where the references to A and B are the same, will always produce object C, where the reference will also be the same. +The current documentation can be found [here](../../framework-base/src/merge-props/README.md).