To propose updates to this style guide, please make the relevant changes in a branch and submit a PR. We review these PRs synchronously in our weekly Team Code Review and collectively gavel on new standards there.
Description
Example
Example
Ensures that TypeScript doesn't prevent people from using the values directly rather than consuming the enum.
export const someConstant = {
optionOne = 'option 1',
optionTwo = 'option 2',
} as const;
export type someConstant = (typeof someConstant)[keyof typeof someConstant];
enum someConstant = {
optionOne: 'option 1',
optionTwo: 'option 2',
}
Maintains organization within files and makes them consistently more readable.
Ensures a consistent API within the system. Allows consumers to add semantic markup and custom styles to child elements.
If a component's children
should be limited to a short string per design guidelines, this should 1. be explicit in the design guidelines, and 2. be restricted via styles rather than with types (e.g. setting a max-height
on an internal element)
interface MyComponentProps {
children: React.ReactNode;
}
const MyComponent = ({ children }: MyComponentProps) => {
return (
<div
className={css`
max-height: 3em;
`}
>
{children}
</div>
);
};
Keeps code cleaner and easier to read.
return isBlah ? a : b;
if (isBlah) {
return a;
} else {
return b;
}
Keeps code easier to read
if (isBlah) {
return 'a';
} else if (isFoo) {
return 'b';
} else {
return 'c';
}
return isBlah ? 'a' : isFoo ? 'b' : 'c';
Documenting functions with TSDoc helps PR reviewers better understand the intent of the function and also documents its use for other engineers using the code base. Better yet, VS Code will automatically show in-line descriptions of a function when a user invokes that function in their editor.
JSDocs should also clearly mention any major assumptions made about the function's input and output that may not be immediately obvious to an engineer unfamiliar with this part of the code base.
/**
* Get the next applicable instance for upgrade. The function handles regions and cloud providers
* but does NOT work for Serverless clusters
* @param clusterDescription the backbone cluster description model
* @param providerOptionsModel the backbone provider options model
* @returns the next upgrade instance for this cluster
*/
export const getNextUpgradeInstance = (
clusterDescription: ClusterDescription,
providerOptionsModel: ProviderOptions,
) => {
// ...
};
export const getNextUpgradeInstance = (
clusterDescription: ClusterDescription,
providerOptionsModel: ProviderOptions,
) => {
// ...
};
Keeps functions pure.
(e.g. shouldX
, isY
, canZ
, doesX
, hasY
, willZ
)
- Prefix with “lg”
- Words are separated by underscore
_
- Blocks are separated by dash
-
- Name hierarchy should somewhat match the directory structure
Additionally, a hard-coded data
attribute should only be placed on a root-level, native HTML element. Components should have their own internal id.
// CalendarCell.tsx
<td data-testid="lg-date_picker-calendar_cell" />
// Calendar.tsx
<Cell data-testid="lg-date-picker-calendar-cell" />
(Note, this is fine to do in a test/spec file. Just don't hard-code this into the component)
BEM uses dashes (-
) to separate words within a block/element, and a double underscore (__
) to separate blocks/elements/modifiers.
The main issue with strict BEM syntax is that it creates a poor user experience when editing. In most text editors a Double click or Option
+ArrowKey
presses treat underscores as one works and dashes as a separator. For example, to replace the "calendar_cell"
part in the above example, you can double click it and paste. Or to move the cursor from the end of that string to the previous element you can press Option
+ ArrowLeft
.
Avoid inline declaration of static variables when they can be moved to top of file or refactored into new file
"const until you can'tst"
Enforces a separation of concerns and helps with readability of both the styles and markup.
const myStyles = css`...`
<div className={myStyles} />
<div className={css`...`} />
When using Emotion css
, postfix your variable with the word styles
(or style
).
When creating a selector with createUniqueClassName
, postfix the variable with className
.
This helps with readability, and makes it easier to tell what a given variable is meant to do.
const myComponentStyles = css``;
const myComponentClassName = createUniqueClassName();
While custom properties (i.e. var(--my-color)
) can be powerful, try to avoid using them since it can be hard to trace the origin of these variables vs. JS variables. Within a single component, prefer using a JS constant to store a style token. If you want to leverage the inheritance afforded by custom properties, consider if using static selectors can solve the problem.
const childClassName = createUniqueClassName();
const parentStyles = css`
.${childClassName} {
color: blue;
}
&:hover .${childClassName} {
color: green;
}
`;
const parentStyles = css`
--color: blue
&:hover {
--color: green;
}
`;
const childStyles = css`
color: var(--color);
`;
Minimizes inline JavaScript logic, and creates a named function that will show up in stack traces. Helps with debugging.
const handleClick = e => {
e.preventDefault();
setState(curr => !curr);
};
<button onClick={handleClick} />;
<button
onClick={e => {
e.preventDefault();
setState(curr => !curr);
}}
/>
Standardizes how we name and search for functions that handle events
const handleClick = () => {};
return <button onClick={handleClick} />;
const onClick = () => {};
return <button onClick={onClick} />;
Consolidates the number of levels of DOM
return <></>;
return <div></div>;
Using cloneElement is uncommon and can lead to fragile code. Prefer render props, context, or custom Hook
Passing a render prop into a component instead of cloning a prop/child is more explicit, and makes it easier to trace a child component's state.
See react.dev for more details.
// MyComponent.tsx
return (
<Menu
renderTrigger={triggerProps => (
<Button {...triggerProps} leftGlyph="Beaker" />
)}
/>
);
// Menu.tsx
const triggerProps = {...}
return (
<>
{renderTrigger(triggerProps)}
</>
);
// MyComponent.tsx
return <Menu trigger={<Button leftGlyph="Beaker">}>;
// Menu.tsx
const triggerProps = {...}
return <>{React.cloneElement(trigger, triggerProps)}</>;
Immutability
function changeValues(object) {
const storedValues = { ...object };
storedValues['newEntry'] = 'updates';
return storedValues;
}
function changeValues(object) {
object['newEntry'] = 'updates';
return object;
}
Ref forwarding allows us to provide direct access to the underlying parent element. For more information on ref forwarding, check out the React docs.
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ children }: ButtonProps, forwardedRef) => (
<button ref={forwardedRef}>{children}</button>
),
);
// This ref will give you direct access to the DOM button
const ref = useRef<null | HTMLButtonElement>(null);
<Button ref={ref}>Click me!</Button>;
const Button = props => <button>{props.children}</button>;
// Does not accept a ref
<Button>Click me!</Button>;
The useDarkMode()
hook allows components to read the value of the darkMode
prop from the LeafyGreenProvider
and overwrite the value locally if necessary.
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ children, darkMode: darkModeProp }: ButtonProps, forwardedRef) => {
const { darkMode, theme, setDarkMode } = useDarkMode(darkModeProp);
return (
<LeafyGreenProvider darkMode={darkMode}>
<button ref={forwardedRef}>{children}</button>
</LeafyGreenProvider>
);
},
);
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ children, darkMode = false }: ButtonProps, forwardedRef) => {
return (
<LeafyGreenProvider darkMode={darkMode}>
<button ref={forwardedRef}>{children}</button>
</LeafyGreenProvider>
);
},
);
Rather than pass darkMode
to each child component, it is recommended to wrap all children in the LeafyGreenProvider
and pass the darkMode
value directly to the LeafyGreenProvider
. All LG components consume the useDarkMode()
hook which reads the value of darkMode
from the innermost LeafyGreenProvider
wrapper.
return (
<LeafyGreenProvider darkMode={darkMode}>
<Button>This is a button</Button>
<Button>This is another button</Button>
</LeafyGreenProvider>
);
return (
<>
<Button darkMode={darkMode}>This is a button</Button>
<Button darkMode={darkMode}>This is another button</Button>
</>
);
- Use
state='error'
to show the input with a warning icon and red border. This property must be set toerror
in order for anerrorMessage
to render, otherwise theerrorMessage
will be ignored. - Use
errorMessage
prop to set the error message that is displayed next to the input. - If
state='error'
buterrorMessage
is not defined, requirearia-describedby