-
Notifications
You must be signed in to change notification settings - Fork 169
JSX runtime (part 2 of 3) - more formal rendering logic for both classic and new jsx runtimes #3947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
cbb6a70
rework jsx helpers for both new and old runtimes
JasonVMo 307bb76
implement both new and old render patterns for classic and jsx-runtimes
JasonVMo f3826ef
Change files
JasonVMo 09f5144
move package README.md files to be next to the source code
JasonVMo cfb86b0
update documentation
JasonVMo 52ac4df
Change files
JasonVMo 50008e7
prettier format updates to documentation
JasonVMo fda5f58
tweak documentation just a bit
JasonVMo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-native-framework-base-5fdded4e-da99-42ac-a660-501e4aa4e831.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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": "[email protected]", | ||
| "dependentChangeType": "patch" | ||
| } |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-native-immutable-merge-fe963967-3c5d-44ee-b7b6-bd67720856d6.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "type": "none", | ||
| "comment": "update documentation", | ||
| "packageName": "@fluentui-react-native/immutable-merge", | ||
| "email": "[email protected]", | ||
| "dependentChangeType": "none" | ||
| } |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-native-memo-cache-f82fa9b0-24fa-493e-90bf-23f4592908f3.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "type": "none", | ||
| "comment": "update documentation", | ||
| "packageName": "@fluentui-react-native/memo-cache", | ||
| "email": "[email protected]", | ||
| "dependentChangeType": "none" | ||
| } |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-native-merge-props-88bde9a7-67b6-40ac-9e3f-cf9e3e643597.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "type": "none", | ||
| "comment": "update documentation", | ||
| "packageName": "@fluentui-react-native/merge-props", | ||
| "email": "[email protected]", | ||
| "dependentChangeType": "none" | ||
| } |
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-native-tester-e3900adc-3730-4ef8-8279-3c0f08e2c8b5.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "type": "patch", | ||
| "comment": "rework jsx helpers for both new and old runtimes", | ||
| "packageName": "@fluentui-react-native/tester", | ||
| "email": "[email protected]", | ||
| "dependentChangeType": "patch" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MyTextProps> = (props) => { | ||
| return <MyText {...props, italicize: true} />; | ||
| } | ||
| ``` | ||
|
|
||
| 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. | ||
JasonVMo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Note that while the newer patterns work without any JSX hooks, the hooks will enable the element flattening. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TProps>(type: RenderType): DirectComponent<TProps> | undefined { | ||
| if (typeof type === 'function' && (type as DirectComponent<TProps>)._callDirect) { | ||
| return type as DirectComponent<TProps>; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| function asLegacyDirectComponent<TProps>(type: RenderType): LegacyDirectComponent<TProps> | undefined { | ||
| if (typeof type === 'function' && (type as LegacyDirectComponent<TProps>)._canCompose) { | ||
| return type as LegacyDirectComponent<TProps>; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| export function renderForJsxRuntime<TProps>( | ||
| type: React.ElementType, | ||
| props: React.PropsWithChildren<TProps>, | ||
| 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<TProps>(type); | ||
| if (directComponent) { | ||
| const newProps = { ...props, key }; | ||
| return directComponent(newProps); | ||
| } | ||
| return jsxFn(type, props, key); | ||
| } | ||
|
|
||
| export function renderForClassicRuntime<TProps>(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; |
114 changes: 114 additions & 0 deletions
114
packages/framework-base/src/component-patterns/render.types.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<typeof ReactJSX.jsx>; | ||
| export type RenderType = Parameters<typeof ReactJSX.jsx>[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<TProps> = (props: TProps) => RenderResult; | ||
|
|
||
| /** | ||
| * The full component definition that has the attached properties to allow the jsx handlers to render it directly. | ||
| */ | ||
| export type DirectComponent<TProps> = DirectComponentFunction<TProps> & { | ||
| 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<TProps> = React.FunctionComponent<TProps> & { | ||
| _canCompose?: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Legacy type name used for consistency with old rendering patterns. | ||
| */ | ||
| export type SlotFn<TProps> = LegacyDirectComponent<TProps>; | ||
|
|
||
| /** | ||
| * 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<TProps> = (props: TProps) => React.ComponentType<React.PropsWithChildren<TProps>>; | ||
|
|
||
| /** | ||
| * Component type for a component that can be rendered in two stages, with the attached render function. | ||
| */ | ||
| export type StagedComponent<TProps> = React.FunctionComponent<TProps> & { | ||
| _twoStageRender?: TwoStageRender<TProps>; | ||
| }; | ||
|
|
||
| /** | ||
| * 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<TProps> = (props: TProps, ...children: React.ReactNode[]) => JSX.Element | null; | ||
|
|
||
| /** | ||
| * Signature for a staged render function. | ||
| * @deprecated Use TwoStageRender instead | ||
| */ | ||
| export type StagedRender<TProps> = (props: TProps, ...args: any[]) => FinalRender<TProps>; | ||
|
|
||
| /** | ||
| * Signature for a component that uses the staged render pattern. | ||
| * @deprecated Use TwoStageRender instead | ||
| */ | ||
| export type ComposableFunction<TProps> = React.FunctionComponent<TProps> & { _staged?: StagedRender<TProps> }; | ||
|
|
||
| /** | ||
| * 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<TProps> = | ||
| | React.FunctionComponent<TProps> | ||
| | DirectComponent<TProps> | ||
| | StagedComponent<TProps> | ||
| | ComposableFunction<TProps> | ||
| | LegacyDirectComponent<TProps>; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.