Code Guidelines

General guidelines

  • Do not mark as required props that are not actually required for a component functionality, this also means that in most cases you do not need to require a property that is also not required by native html elements.

  • Leverage existing Primitive libraries, do not try to reinvent the wheel.

  • Do not use any

  • Add the correct types to the component callback functions.

  • Avoid declaring components inside of other components.

  • Do not use nested ternary operators. An if else clause is easier to read and maintain.

  • Use consistent casing.

  • If your component needs to place UI on top of other elements that you do not control, use React Portals.

Leverage a Primitive library

Use low-level UI component libraries as the base layer of the design system, this way we can avoid writing inaccessible and non-performant components.

To achieve this we use radix-ui. Example:

import React from 'react';
import * as BaseTooltip from '@radix-ui/react-tooltip';
import './styles.scss';

const Tooltip = ({ message, children }) => {
  return (
    <BaseTooltip.Provider>
      <BaseTooltip.Root>
        <BaseTooltip.Trigger asChild>
         {children}
        </BaseTooltip.Trigger>
        <BaseTooltip.Portal>
          <BaseTooltip.Content className="sdsTooltip" sideOffset={5}>
            {content}
            <BaseTooltip.Arrow className="sdsTooltip_arrow" />
          </BaseTooltip.Content>
        </BaseTooltip.Portal>
      </BaseTooltip.Root>
    </BaseTooltip.Provider>
  );
};

export default Tooltip;

Don't reimplement or rewrite native html props

Leverage existing props from HTML or from the Primitives library.

Instead of writing

type Props = {
    value: string;
    onChange: unknown;
    onBlur: unknown;
    onClick: unknown
};
const Input = (props: Props) => ();

You can do this:

type Props = React.HTMLProps<HTMLInputElement>;
const Input = (props: Props) => ();

If you need to add new props:

type CustomProps = {
  helpText: string;
  error: boolean;
  errorMessage: string;
};
type Props = Partial<CustomProps> & React.HTMLProps<HTMLInputElement>

If you don't support or need all the native HTML props:

type OmittedProps = 'disabled' | 'readonly'
type Props = Omit<React.HTMLProps<HTMLInputElement>, OmittedProps>

Export a single file or object for a component

If your component has multiple parts that the user will have to compose, export them from a single object instead of having multiple exports.

Instead of:

// ModalRoot index.ts
export ModalRoot from './ModalRoot';
// ModalContent index.ts
export ModalContent from './ModalContent';
// ModalFooter index.ts
export ModalFooter from './ModalFooter';

Export:

// Modal index.ts
const Modal = {
    Root,
    Content,
    Footer
}

Avoid using useEffect

The react docs explain it better than we can do, but here are some general guidelines:

  • If you can calculate something during render, you don’t need an Effect.

  • To cache expensive calculations, add useMemo instead of useEffect.

  • To reset the state of an entire component tree, pass a different key to it.

  • To reset a particular bit of state in response to a prop change, set it during rendering.

  • Code that needs to run because a component was displayed should be in Effects, the rest should be in events.

  • If you need to update the state of several components, it’s better to do it during a single event.

  • Whenever you try to synchronize state variables in different components, consider lifting state up.

  • You can fetch data with Effects, but you need to implement cleanup to avoid race conditions.

Transforming data for rendering in useEffect is inefficient:

When you update your component’s state, React will first call your component functions to calculate what should be on the screen. Then, React will “commit” these changes to the DOM, updating the screen. Then React will run your Effects. If your Effect also immediately updates the state, this restarts the whole process from scratch! To avoid the unnecessary render passes, transform all the data at the top level of your components. That code will automatically re-run whenever your props or state change.

Testing

To test we use react-testing-library, this allows us to write maintainable tests and avoid including implementation details. Test component behavior and ways the user can interact with the component. An example for the Input component would be the following tests:

  • can be rendered in an uncontrolled way

  • will trigger onChange when used as a uncontrolled component

  • will only trigger the onChange event if the user has interacted with it

  • onChange will not trigger with a disabled input

  • onChange will not trigger with a read only input

  • onChange will work properly with a debounced function

Examples for a Dropdown:

  • show options on click

  • show default value with no selection

  • if options are visible, hide them when user clicks outside

  • on selection, show selected option in dropdown trigger

Do not test implementation details, this means that you should avoid testing component inner logic like:

  • state values

  • hook runs

  • results of parser functions inside the component

Stateful components can be controlled or uncontrolled (borrowed from radix-ui)

Similar to form field JSX elements in React, all components with internal state can either be uncontrolled (internally managed) or controlled (managed by the consumer)

Components exist in a finite number of predefined states (borrowed from radix-ui)

  • State in this context refers to a component's state representable by a finite state machine; not to be confused with arbitrary stateful data as typically referenced in React libraries.

  • States are predetermined during the component design phase and expressed as strings in component code, making state transitions more explicit, deterministic, and clearer to follow.

  • Use the data-state attribute to expose a component's state directly to its DOM element.

  • When tempted to use a boolean to track a piece of stateful data, consider enumerated strings instead

<div data-error="true" data-disabled="true" class="container">
    <input disabled />
    <p>Error</p
</div>

<style>
.container[data-error] {
    color: red;
}

input:disabled {
    color: grey;
}
</style>

Developer experience (borrowed from radix-ui)

  • Component APIs should be relatively intuitive and as declarative as possible.

  • Provide in-code documentation for complex/unclear abstractions for easier source debugging.

  • Anticipate errors and provide thorough console warnings with links back to documentation.

const ModalFooter = ({ children }) => {
  if (beingRenderedOutsideOfModalRoot) {
    console.warn('render it correctly ');
  }

  const styles = {};
  if (isDev && beingRenderedOutsideOfModalRoot) {
    styles.borderColor = 'red';
  }

  return (
    <div
      style={styles}
      className={classNames('sds_Modal_Container_Footer', {
        [String(className)]: className,
      })}>
      {children}
    </div>
  );
};

export default ModalFooter;

Last updated