Implementation Best Practices
These best practices apply primarily to v9+ components (@fluentui/react-components
).
Consistent property names across the component library help developers understand how to use your component more quickly because the names are familiar. When there aren't good examples, check out open-ui.org to see what names are commonly used across UI component libraries and frameworks.
For example, a property that controls the overall layout and style of a component is most commonly named appearance
. Avoid naming your property display
, look
, etc.
This is true for discriminated union values too. For example, sizes is most commonly 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large'
. There are two common outlier sizes tiny
and jumbo
. Avoid naming sizes ex-s
or hugely
.
If a property applies to the component or to the logical element of the component choose an unadorned name. Avoid prefixing the component name (e.g. prefer icon
over buttonIcon
) or unnecessary adjectives (e.g. prefer shape
over overallShape
).
If a property applies to a part or slot of a component, prefix/suffix with the part name (e.g. prefer iconPosition
over position
). Prefer to prefix the part name except where the property is acting as a verb (e.g. alignContent
over contentAlign
).
Avoid any hungarian notation of properties. While they can appear helpful when writing the props in Typescript, they are not idiomatic when calling from TSX.
Trying to logically group properties is too sensitive to each developers preferences. Listing alphabetically allows developers to easily scan for a property. It also matches with how the API and storybook documentation will list properties.
If your component provides a forwardRef
to a native element (e.g. Button => ) and you intersect React.HTMLAttributes<>
, avoid re-declaring those attributes within your component props. Doing so can lead to type mismatch problems.
Avoid names that are commonly used in other HTML elements or attributes. They won't cause a compile or runtime error, but they may be confusing to callers.
Part of the design specification process for a component is to identify and name the parts that make up a component visually. When defining properties representing slots for these parts, use the name as specified.
v9+ components are built using React Hooks and Typescript. Hooks is a functional programming approach that replaces the the object-oriented approach of React component classes. While Typescript supports object-oriented programming through interfaces and classes, it also fully supports functional programming through type declarations, type intersections, and discriminated unions. Because Typescript transpiles to JavaScript, a prototype-based language, it provide type inference and duck typing.
Types and interfaces can often be used interchangeably in Typescript. There are a few differences that make types the preferred choice:
-
Interfaces can be re-opened to add new properties through declaration merging.
This is something to avoid with component props. Props form the API contract of the component and should change with the component. If someone adds a required property to an interface, all extensions must implement it. It is a compile error if subsequent property declarations define the same property with a different type.
-
Type aliases can rename primitives.
This can help to provide more meaning about a property to developer. For example an ID property could be declared as
id : string;
, butid: Base64EncodedString
provides much more information. -
Types can declare and intersect with discriminated unions.
Discriminated unions are helpful when using components through TSX/JSX from a component library. The various attributes on the element that control the component are strings that need to be one-of from a set of values.
position: 'before' | 'after'
for example.
Boolean properties are convenient to set in TSX and are appropriate for flag values.
For example, a Tooltip component may have an option to show an arrow pointing at the target. withArrow: boolean
. Callers can easily set this in TSX <Tooltip withArrow />
.
If there are properties that are mutually exclusive, then a discriminated union is a better choice. The caller cannot accidentally specify multiple values which could lead to unpredictable behavior.
Going back to the Tooltip component, it might have different options for position relative to the target. If these were boolean values like before?: boolean
, after?: boolean
, or cover?: boolean
, the caller could write <Tooltip before after cover />
. It is much clearer to specify <Tooltip position="before" />
.
If you have only a single optional value today and think there could be multiple values in the future, prefer a discriminated union with one value. For example: border: boolean
would have to be changed to a discriminated union later to support both square, rounded, and circular borders.
When a property type is a discriminated union, it is tempting to declare and export a type for it.
For example a size, 'small' | 'medium' | 'large'
would be easy to define as export type Size = 'small' | 'medium' | 'large'
and declare size: Size;
.
There are few pitfalls with defining and exporting types for every discriminated union:
-
Tightly bound cross-component dependencies.
If a component A uses the Size type from component B and then component B adds a value to the union, it could break component A. Component A is also less portable.
-
Extra F12 navigation
Developers inspecting the props have to follow the union to understand it rather than seeing the union right next to the property. This can lead to duplicate documentation comments on property and type declarations.
-
Type explosion/collision in the type declaration files (
.d.ts
).While types are compiled away, having many types used only in one place increases declaration file size without much benefit.
Component authors have to be careful to distinguish their type from other component types (e.g. ButtonSize vs. ImageSize). Otherwise tooling might auto-import the incorrect type.
Since the practice is to place types in separate
.types.ts
files, these get exported for the component to use and can be inadvertently exported byindex.ts
export * from ...
There are scenarios when you should consider defining a type for a discriminated union:
- It is used across multiple components within the same package, or multiple times in the same props declaration.
- There are many union values that it clutters the props declaration.
- The type is a union of unions (e.g.
color: 'brand' | 'neutral' | StatusColor
) - Callers will need to use the type in TS/JS rather than in just TSX/JSX.
- Developers authoring variants of your component will need that type in their component props.
- FAQ - Fabric and Stardust to Fluent UI
-
@fluentui/react
Version 9 -
@fluentui/react
Version 8 - Contributing to the
7.0
branch - How to apply themes (version 7/8)
- Planning and development process (for work by the core team)
- Conducting meetings Style guide
- Keeping up with review requests
- RFC review process
- Setup (configuring your environment)
- Fluent UI React version 7/8
- CLA
- Overview
- Repo structure
- Development process
- Contributing to previous versions
- API Extractor
- Build command changes made in early 2020
- Component implementation guide
- Creating a component
- Implementation Best Practices
- Theming
- Documenting
- Styling (old approach)
- Overview
- Testing with Jest
- E2E testing (Cypress)
- Visual testing (Screener)
- Accessibility review checklist