0%

So what if you want to type defaultProps? We’ll use the Bootstrap Badge component as an example. According to the Typescript React cheatsheet, when we use Typescript > 3.0, we have to use type inference:

import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { Variant } from './Bootstrap';

const defaultProps = {
  pill: false,
};

export type BadgeProps = {
  children: ReactNode;
  variant: Variant;
} & typeof defaultProps;

const BS_ROOT = 'badge';

function Badge({ children, variant, pill }: BadgeProps): JSX.Element {
  return (
    <span
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`, {
        [`${BS_ROOT}-pill`]: pill,
      })}
    >
      {children}
    </span>
  );
}

Badge.defaultProps = defaultProps;

export default Badge;

But imho, this looses the clear distinction between your component’s required and optional props! I would rather have the following BadgeProps type:

export type BadgeProps = {
  children: ReactNode;
  variant: Variant;
  pill?: boolean;
};

An alternative approach

What if we would just use object default values?

import React, { ReactNode } from 'react';
import classNames from 'classnames';
import { Variant } from './Bootstrap';

export type BadgeProps = {
  children: ReactNode;
  variant: Variant;
  pill?: boolean;
};

const BS_ROOT = 'badge';

function Badge({ children, variant, pill = false }: BadgeProps): JSX.Element {
  return (
    <span
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`, {
        [`${BS_ROOT}-pill`]: pill,
      })}
    >
      {children}
    </span>
  );
}

export default Badge;

See easier and simpler (KISS) and not only that according to a tweet of Dan Abramov, defaultProps will be eventually deprecated for functional components.

To keep in the spirit of honing our skills, we’ll tackle one more component, a button.

So let’s start with the basics, a <button /> has children, an onClick handler, a type (submit, button or reset) and can be disabled.

One thing to keep in mind with buttons is that if you don’t supply a type, it will default to submit which is generally not what you want.

import React, { ReactNode } from 'react';

export type ButtonProps = {
  children: ReactNode;
  disabled?: boolean;
  onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
  type: 'submit' | 'reset' | 'button';
};

function Button(props: ButtonProps): JSX.Element {
  const { children, disabled, onClick, type } = props;

  return (
    <button disabled={disabled} onClick={onClick} type={type}>
      {children}
    </button>
  );
}

export default Button;

Using the Pick utility type we could shorten the ButtonProps to the following:

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'children' | 'disabled' | 'onClick'
> & {
  /**
   * The reason we are not picking this is because react allows undefined.
   */
  type: 'button' | 'submit' | 'reset';
};

Touch

We just need to add that bootstrap flavor, let’s add variant into the mix:

import React from 'react';
import classNames from 'classnames';

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'children' | 'disabled' | 'onClick'
> & {
  /**
   * The reason we are not picking this is because react allows undefined.
   */
  type: 'button' | 'submit' | 'reset';
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark'
    | 'link';
};
const BS_ROOT = 'btn';

function Button(props: ButtonProps): JSX.Element {
  const { children, disabled, onClick, type, variant } = props;

  return (
    <button
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`)}
      disabled={disabled}
      onClick={onClick}
      type={type}
    >
      {children}
    </button>
  );
}

export default Button;

Satisfaction

But what if you wanted a “close” button which just an x inside it as children?

import React from 'react';
import Alert from './components/Button';

function App(): JSX.Element {
  return <Button type="button">&times;</Button>;
}

We should always have accessibility in mind when we create components, a button without a readable text is not accessible. The correct approach is to add an aria-label to the button.

And as an added advantage if you’re using React Testing Library, you can use a ByLabelText query and don’t have to add a data-testid to a button.

So we want to achieve this:

import React from 'react';
import Alert from './components/Button';

function App(): JSX.Element {
  return (
    <Button aria-label="Close" type="button">
      &times;
    </Button>
  );
}

We can again pick the aria-label from the button properties.

export type ButtonProps = Pick<
  React.DetailedHTMLProps<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    HTMLButtonElement
  >,
  'aria-label' | 'children' | 'disabled' | 'onClick'
> & {
  type: 'button' | 'submit' | 'reset';
  variant: 'primary'; //shortened
};

const BS_ROOT = 'btn';

function Button(props: ButtonProps): JSX.Element {
  const {
    'aria-label': ariaLabel,
    children,
    disabled,
    onClick,
    type,
    variant,
  } = props;

  return (
    <button
      aria-label={ariaLabel}
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`)}
      disabled={disabled}
      onClick={onClick}
      type={type}
    >
      {children}
    </button>
  );
}

That’s it, i hope you are digging the typescript/react series so far.

When i want to practice componentization, i usually look for a candidate in a library like Bootstrap. The nice thing about these exercises is that if you create the same component more than once, you can gradually see the progress you’ve made. This time we’ll practice on the Alert component.

Basic

So according to the documentation an alert, in it’s most basic form, looks like this:

<div class="alert alert-primary" role="alert">
  A simple primary alert—check it out!
</div>
  • a div with alert role
  • root class: alert
  • contextual classes: primary, secondary, success, danger, warning, info, light and dark (alert-{contextual} class)

Result

import React, { ReactNode } from 'react';

export type AlertProps = {
  children: ReactNode;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({ children, variant }: AlertProps): JSX.Element {
  return (
    <div role="alert" className={`${BS_ROOT} ${BS_ROOT}-${variant}`}>
      {children}
    </div>
  );
}

export default Alert;

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return <Alert variant="primary">A simple primary alert—check it out!</Alert>;
}

Heading

<div class="alert alert-success" role="alert">
  <h4 class="alert-heading">Well done!</h4>
  <p>
    You made it.
  </p>
</div>

As there is no extra behavior tied to the heading, a string property looks like a perfect match otherwise an utility component <AlertHeader /> would have been in order.

Result

import React, { ReactNode } from 'react';

export type AlertProps = {
  children: ReactNode;
  heading?: string;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({ children, variant, heading }: AlertProps): JSX.Element {
  return (
    <div role="alert" className={`${BS_ROOT} ${BS_ROOT}-${variant}`}>
      {heading && <h4 className={`${BS_ROOT}-heading`}>{heading}</h4>}
      {children}
    </div>
  );
}

export default Alert;

And if you do need to have more fine-grained control of the html of the heading, you could transform the heading property into a slot, by changing the type from string to ReactNode.

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return (
    <Alert heading="Well done!" variant="success">
      <p>You made it!</p>
    </Alert>
  );
}

Dismissing

<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <strong>Holy guacamole!</strong> You should check in on some of those fields
  below.
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
  • a button with the class close
  • the alert needs to be enriched with the alert-dismissible class
  • when the user clicks on the dismiss button the alert should no longer be visible

I choose not to use the Bootstrap JS Library and let the alert itself control the dismissed state, if you ever need to do something more than just hiding the alert, you can always add an optional onAfterDismiss function property.

Result

import React, { ReactNode, useState } from 'react';
import classNames from 'classnames';

export type AlertProps = {
  children: ReactNode;
  dismissible?: boolean;
  heading?: string;
  variant:
    | 'primary'
    | 'secondary'
    | 'success'
    | 'danger'
    | 'warning'
    | 'info'
    | 'light'
    | 'dark';
};

const BS_ROOT = 'alert';

function Alert({
  children,
  dismissible,
  heading,
  variant,
}: AlertProps): JSX.Element {
  const [dismissed, setDismissed] = useState(false);

  return (
    <div
      className={classNames(BS_ROOT, `${BS_ROOT}-${variant}`, {
        [`${BS_ROOT}-dismissible`]: dismissible,
      })}
      style={{ display: dismissed ? 'none' : undefined }}
      role="alert"
    >
      {heading && <h4 className={`${BS_ROOT}-heading`}>{heading}</h4>}
      {children}
      {dismissible && (
        <button
          type="button"
          className="close"
          aria-label="Close"
          onClick={() => setDismissed(true)}
        >
          <span aria-hidden="true">&times;</span>
        </button>
      )}
    </div>
  );
}

export default Alert;

Usage

import React from 'react';
import Alert from './components/Alerts/Alert';

function App(): JSX.Element {
  return (
    <Alert variant="warning" dismissible>
      <strong>Holy guacamole!</strong> You should check in on some of those
      fields below.
    </Alert>
  );
}

If you are interested in my setup or in the tests using React Testing Library you can take a look at my React Playground Repository.

As Javascript continues to be one of the most misunderstood languages on the planet, of which you could find evidence all over the web. One of my personal favorites is the lightning Wat talk by Gary Bernhardt.

Lately i’ve begun to realize the underlying beauty of the language and felt the need to blog about it.

We’ll start with two primitives, undefined and null. Why do they exist both and what differentiates them?

undefined

The value that gets assigned to unassigned declared variables or to function parameters that are missing.

// 1. Unassigned variable
let variable;
console.log(variable); // => undefined

// 2. Missing function parameter
function greet(name) {
  console.log(name);
}

greet(); // => undefined

null

Represents the intentional absence of a value, it’s a semantical difference.

const notDotted = 'googlebe';
console.log(notDotted.match(/\w+\.\w+/)); // => null

Are they equal?

console.log(undefined === null); // => false

So strictly speaking they are different, which we could have expected. But they do have the same meaning:

console.log(undefined == null); // => true

Falsy

Luckily they both evaluate to false, otherwise we would have to write the following code:

let user;

// 💩 If they didn't evaluate to false and without ==
if (user !== null && user !== undefined) user.rename('John', 'Doe');

// 💪 With == (type coercion)
if (user != null) user.rename('John', 'Doe');

// 🙏 Thanks to falsy
if (user) user.rename('John', 'Doe');

Conclusion

Concepts like type coercion and falsy start to make a whole lot of sense if you look at the problems they fix.

In React Testing Library, the recommended way, after the other queries don’t work for your use-case, is to add a data-testid attribute on the element.

This works for all baked-in React HTML components, for instance on a <div/>:

import React from 'react';

export default function Component() {
  return <div data-testid="some-test-id" />;
}

Spec

import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render } from '@testing-library/react';
import Component from './Component';

test('it renders as expected', () => {
  const { getByTestId } = render(<Component />);

  expect(getByTestId('some-test-id')).toBeInTheDocument();
});

How to do this with custom components?

Due to the - in the name it’s not so easy and you will see other property names for it or just hard-coding the the testId into the component:

import React from 'react';
import { string } from 'prop-types';

export default function Component({ testId }) {
  return <div data-testid={testId} />;
}

Component.propTypes = {
  testId: string,
};

Component.defaultProps = {
  testId: undefined,
};

Spec

test('it renders as expected', () => {
  const testId = 'some-test-id';

  const { getByTestId } = render(<Component testId={testId} />);

  expect(getByTestId(testId)).toBeInTheDocument();
});

Preferred approach

As i don’t like having two different conventions for specifying the data-testid attribute, i would suggest the following:

import React from 'react';
import { string } from 'prop-types';

export default function Component({ 'data-testid': dataTestId }) {
  return <div data-testid={dataTestId} />;
}

Component.propTypes = {
  'data-testid': string,
};

Component.defaultProps = {
  'data-testid': undefined,
};

Spec

test('it renders as expected', () => {
  const testId = 'some-test-id';

  const { getByTestId } = render(<Component data-testid={testId} />);

  expect(getByTestId(testId)).toBeInTheDocument();
});

Now you can use data-testid for baked-in HTML components as well as your custom components.