docs/react-v9/contributing/rfcs/react-components/convergence/converged-implementation-patterns.md
List contributors to the proposal here
Tries to commit the common component pattern to writing in the repo for future reference and track improvements.
Code samples should applied to utility scripts to generate packages which will help to educate/enforce this pattern... until we want to change it :-)
Many new engineers can find it hard to adapt to a new codebase, especially when the coding patterns and practices are fairly new and in a constant state of flux.
Many new developers might have questions on the patterns that we follow to write a React component. This RFC attempts to commit and possibly improve our code patterns in writing so that there might be a reference for new (and old) members of the team.
Generally a component will have the following different files. Let us consider a Sample component, it will contain following parts:
Sample.tsxuseSample.tsuseSampleStyles.tsrenderSample.tsxSample.types.tsThe 'final' product, simply forwards a ref and uses all the previous building blocks to expose the final component
export const Sample = React.forwardRef<HTMLElement, SampleProps>((props, ref) => {
const state = useSample(props, ref);
useSampleStyles(state);
return renderSample(state);
});
Accepts the component props and handles the internal state that the component might need.
State here can be pretty broad, you could also consume context or create side effects. This hook should be what the component relies on to function/render. All state operations will mutate draftState object.
import { makeMergeProps, resolveShorthandProps } from '@fluentui/react-utilities';
/**
* Defines the different slots that can be rendered in this component
*
* This means that an icon can be rendered 'somewhere'
*/
export const sampleShorthandProps = ['icon'];
// Creates a helper function to merge props while respecting slot props
const mergeProps = makeMergeProps<SampleState>({ deepMerge: sampleShorthandProps });
/**
* @parameter props -> these are normal React props for JSX components
* @parameter ref -> In case someone wants a ref to the root DOM element
* @parameter defaultProps -> safe default props, we set defaults internally but if consumer wants component variants with different defaults, they can set them
*/
export const useSample = (props: SampleProps, ref: React.Ref<HTMLElement>, defaultProps?: SampleProps): SampleState => {
const resolvedRef = useMergedRefs(ref, React.useRef());
// merges the props we declare internally and what is passed in
// by a consumer
const state = mergeProps(
{
// This is essentially shorthand for the `root` slot
ref: resolvedRef,
as: 'div',
icon: { as: 'span' },
},
defaultProps,
resolveShorthandProps(props, sampleShorthandProps),
);
if (checkedValues || onCheckedValuesChange) {
state.hasCheckMark = true;
}
// Anything else the component needs to manage its own state and render
[someState, setSomeState] = React.useState();
const { contextValue } = React.useContext();
React.useEffect(...);
// enlarge the 'uber' state
state.someState = someState;
state.contextValue = contextValue;
return state;
};
Hook that accepts state and applies classnames to props and any shorthand slot props to style the component and its slots.
import { makeStyles, mergeClasses } from '@fluentui/react-make-styles';
import { SampleState } from './Sample.types';
/**
* Styles for the root slot
*/
export const useRootStyles = makeStyles<SampleState>([
[
null,
theme => ({
backgroundColor: theme['select']['from']['theme']['docs'],
paddingRight: '12px',
}),
],
]);
/**
* Styles for the icon slot, uses state selectors from SampleState
*/
export const useIconStyles = makeStyles<SampleState>([
[
// Conditionally apply styles
someState => someState == 1,
() => ({
width: '20px',
height: '20px',
marginRight: '9px',
}),
],
]);
/**
* Applies style classnames to slots
*/
export const useSampleStyles = (state: SampleState) => {
const rootClassName = useRootStyles(state);
const iconClassName = useIconStyles(state);
// mergeClasses is a util that deduplicates classnames
state.className = mergeClasses(rootClassName, state.className);
if (state.icon) {
state.icon.className = mergeClasses(iconClassName, state.icon.className);
}
};
Renders the correct JSX output of the component and its slots given the correct state.
This should be a pure function whose sole responsibility is to render JSX from the provided state. No state mutation or processing
should happen in this function, but rather done in the useSample hook
import { getSlots } from '@fluentui/react-utilities';
import { sampleShorthandProps } from './useSample';
// state should come from `useSample` hook
export const renderSample = (state: SampleState) => {
const { slots, slotProps } = getSlots(state, sampleShorthandProps);
return (
<slots.root {...slotProps.root}>
<slot.icon {...slotProps.icon} />
</slots.root>
);
};
The state stores all information on the slots/tags/JSX that should be rendered after processing by the relevant hook. The getSlots utility extracts known slot information such as tag names and props.
See below for how state might contain useful props for rendering.
The below will probably be present in every component, other utility types are fair game if they are necessary.
import * as React from 'react';
import { ComponentProps, ShorthandProps, ObjectShorthandProps } from '@fluentui/react-utilities';
// For a component all HTML attributes should be allowed to maximize consistencty with DOM
export interface SampleProps extends ComponentProps, React.HTMLAttributes<HTMLElement> {
/** Icon slot as a shorthand */
icon?: ShorthandProps;
}
// Component state extends props and also adds other internal state used in the lifecycle of the component
// For example for a popup component tracking an `open` state as boolean
export interface SampleState extends SampleProps {
ref: React.MutableRefObject<HTMLElement>;
// once a slot is process in state it can only be an object
icon: ObjectShorthandProps;
open: boolean;
otherState: object;
otherState: number;
}
NOTE: Currently ComponentProp inherits from a GenericDictionary that is essentially Record<string, any> which is essentially any :-(
see this comment thread
Of the above, we expect at least the following to be exported as the three pillars of a component:
<Sample /> - componentrenderSample - render functionuseSample - component state hookSince there is a separation of state, render, styling into different hooks/utility functions, it can be very easy for consumers to create their own custom component variants without needing to approach the library team and accommodate their custom requirements.
It can be very easy to unit test each separate concern without too much wireframing/mocking.
Can be verbose, and might be too much so for simple components without lots of complex interaction.
The use of useSampleState to merge props and state to become one 'uber' state seems to be a break of encapsulation and can cause confusion as to what is props and what is internal state.