How to Create a Great User Experience with React, TypeScript, and the React Testing Library

I’m always willing to learn, no matter how much I know. As a software engineer, my thirst for knowledge has increased a lot. I know that I have a lot of things to learn daily.

But before I could learn more, I wanted to master the fundamentals. To make myself a better developer, I wanted to understand more about how to create great product experiences.

This post is my attempt to illustrate a Proof of Concept (PoC) I built to try out some ideas.

I had some topics in mind for this project. It needed to:

  • Use high-quality software
  • Provide a great user experience

When I say high-quality software, this can mean so many different things. But I wanted to focus on three parts:

  • Clean Code: Strive to write human-readable code that is easy to read and simple to maintain. Separate responsibility for functions and components.
  • Good test coverage: It’s actually not about coverage. It’s about tests that cover important parts of components’ behavior without knowing too much about implementation details.
  • Consistent state management: I wanted to build with software that enables the app to have consistent data. Predictability is important.

User experience was the main focus of this PoC. The software and techniques would be the foundation that enabled a good experience for users.

To make the state consistent, I wanted a type system. So I chose TypeScript. This was my first time using Typescript with React. This project also allowed me to build custom hooks and test it properly.

Setting up the project

I came across this library called tsdx that sets up all the Typescript configuration for you. It’s mainly used to build packages. Since this was a simple side project, I didn’t mind giving it a try.

After installing it, I chose the React template and I was ready to code. But before the fun part, I wanted to set up the test configuration too. I used the React Testing Library as the main library together with jest-dom to provide some awesome custom methods (I really like the toBeInTheDocument matcher).

With all that installed, I overwrote the jest config by adding a new jest.config.js:

module.exports = {
  verbose: true,
  setupFilesAfterEnv: ["./setupTests.ts"],
};

And a setupTests.ts to import everything I needed.

import "@testing-library/jest-dom";

In this case, I just had the jest-dom library to import. That way, I didn’t need to import this package in my test files. Now it worked out of the box.

To test this installation and configuration, I built a simple component:

export const Thing = () => <h1>I'm TK</h1>;

In my test, I wanted to render it and see if it was in the DOM.

import React from 'react';
import { render } from '@testing-library/react';
import { Thing } from '../index';

describe('Thing', () => {
  it('renders the correct text in the document', () => {
    const { getByText } = render(<Thing />);

    expect(getByText("I'm TK")).toBeInTheDocument();
  });
});

Now we are ready for the next step.

Configuring routes

Here I wanted to have only two routes for now. The home page and the search page - even though I’ll do nothing about the home page.

For this project, I’m using the react-router-dom library to handle all things router-related. It’s simple, easy, and fun to work with.

After installing it, I added the router components in the app.typescript.

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

export const App = () => (
  <Router>
    <Switch>
      <Route path="/search">
        <h1>It's the search!</h1>
      </Route>
      <Route path="/">
        <h1>It's Home</h1>
      </Route>
    </Switch>
  </Router>
);

Now if we enter the localhost:1234, we see the title It's Home. Go to the localhost:1234/search, and we’ll see the text It's the search!.

Before we continue to start implementing our search page, I wanted to build a simple menu to switch between home and search pages without manipulating the URL. For this project, I’m using Material UI to build the UI foundation.

For now, we are just installing the @material-ui/core.

To build the menu, we have the button to open the menu options. In this case they’re the “home” and “search” options.

But to build a better component abstraction, I prefer to hide the content (link and label) for the menu items and make the Menu component receive this data as a prop. This way, the menu doesn’t know about the items, it will just iterate through the items list and render them.

It looks like this:

import React, { Fragment, useState, MouseEvent } from 'react';
import { Link } from 'react-router-dom';
import Button from '@material-ui/core/Button';
import MuiMenu from '@material-ui/core/Menu';
import MuiMenuItem from '@material-ui/core/MenuItem';

import { MenuItem } from '../../types/MenuItem';

type MenuPropsType = { menuItems: MenuItem[] };

export const Menu = ({ menuItems }: MenuPropsType) => {
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);

  const handleClick = (event: MouseEvent<HTMLButtonElement>): void => {
    setAnchorEl(event.currentTarget);
  };

  const handleClose = (): void => {
    setAnchorEl(null);
  };

  return (
    <Fragment>
      <Button aria-controls="menu" aria-haspopup="true" onClick={handleClick}>
        Open Menu
      </Button>
      <MuiMenu
        id="simple-menu"
        anchorEl={anchorEl}
        keepMounted
        open={Boolean(anchorEl)}
        onClose={handleClose}
      >
        {menuItems.map((item: MenuItem) => (
          <Link to={item.linkTo} onClick={handleClose} key={item.key}>
            <MuiMenuItem>{item.label}</MuiMenuItem>
          </Link>
        ))}
      </MuiMenu>
    </Fragment>
  );
};

export default Menu;

Don’t panic! I know it is a huge block of code, but it is pretty simple. the Fragment wrap the Button and MuiMenu (Mui stands for Material UI. I needed to rename the component because the component I’m building is also called menu).

It receives the menuItems as a prop and maps through it to build the menu item wrapped by the Link component. Link is a component from react-router to link to a given URL.

The menu behavior is also simple: we bind the handleClick function to the button’s onClick. That way, we can change anchorEl when the button is triggered (or clicked if you prefer). The anchorEl is just a component state that represents the Mui menu element to open the menu switch. So it will open the menu items to let the user chooses one of those.

Now, how do we use this component?

import { Menu } from './components/Menu';
import { MenuItem } from './types/MenuItem';

const menuItems: MenuItem[] = [
  {
    linkTo: '/',
    label: 'Home',
    key: 'link-to-home',
  },
  {
    linkTo: '/search',
    label: 'Search',
    key: 'link-to-search',
  },
];

<Menu menuItems={menuItems} />

The menuItems is a list of objects. The object has the correct contract expected by the Menu component. The type MenuItem ensures that the contract is correct. It is just a Typescript type:

export type MenuItem = {
  linkTo: string;
  label: string;
  key: string;
};

#react #typescript #programming

How to Create a Great User Experience with React, TypeScript
10.00 GEEK