Good Practices for Building a React TypeScript Web Application

Good Practices for Architecting a React TypeScript Web Application

React TypeScript

Building a React TypeScript web application involves more than just writing code. It requires careful planning and architecture to ensure that the application is maintainable, scalable, and efficient. Here are some good practices I've found when architecting a React TypeScript web application.

Use a Consistent Directory Structure

A consistent directory structure makes it easier to navigate your codebase and find specific components or modules. Group related files together, such as placing a component and its associated styles and tests in the same directory.

As an example, here's a sample directory structure for a React TypeScript web application:

src/
  components/
    Button/
      Button.tsx
      Button.module.css
      Button.test.tsx
  pages/
    index.tsx
  styles/
    global.css
  types/
    index.ts
  utils/
    index.ts

Leverage TypeScript's Static Typing

TypeScript's static typing can help catch errors at compile time, making your code more robust. Use TypeScript interfaces and types to define the shape of your data, and use TypeScript's strict mode to enforce type checking.

I like to define my types in a types directory, and then import them into my components and modules as needed. For example, here's a sample types/index.ts file:

export interface User {
  id: number
  name: string
  email: string
}
import { User } from '@/types'

const user: User = {
  id: 1,
  name: 'John Doe',
  email: 'john.doe@test.com',
}

This is not a hard-and-fast rule, but I find it helps keep my code organized and makes it easier to find and update types.

There are a number of times where you only need a one-off type (maybe for defining the shape of an API response), and in those cases, I'll define the type directly in the component or module where it's used.

Data Consistency with TypeScript

I expect data from the API to be inconsistent. This can happen for a variety of reasons, such as a change in the API or a bug in the API.

With that said, once data have been received from the API layer, I expect it to be consistent. I use TypeScript's interface type to define the shape of the data, and then leverage that interface type to define the shape of the data.

interface User {
  id: number
  name: string
  email: string
}

const user: User = {
  id: 1,
  name: 'John Doe',
  email: '',
}

In the case above, even though the email property is an empty string, TypeScript will still enforce that the email property is a string.

Use Functional Components and Hooks

React's functional components and hooks provide a simpler and more intuitive API for building components. Use functional components and hooks whenever possible, and reserve class components for when you need to use lifecycle methods or state.

Move Away From Class Components

Class components are still supported in React, but they're considered legacy and will likely be deprecated in the future. If you're still using class components, consider migrating to functional components and hooks.

Move Away From Redux State Management

Redux is a popular state management library for React, but it's not without its drawbacks. Redux can be difficult to learn and use, and it can be overkill for smaller applications. Consider using React's built-in context API for shared state, and only use Redux if you need to manage complex state.

Even so, it can get incredibly unwieldy to manage state in a large application. If you find yourself in this situation, consider using a state management library like Redux or MobX.

Keep Components Small and Focused

Each component should have a single responsibility. Keeping components small and focused makes them easier to understand, test, and reuse.

Sometimes this isn't possible, and you'll need to create a larger component. Even in those cases, consider breaking up the component and leverage a shared context or state.

Manage State Effectively

Use local state for data that's specific to a single component, and use context or a state management library like Redux for shared state. Be mindful of when to use state vs props, and avoid unnecessary re-renders by using React's useMemo and useCallback hooks.

Exercise Caution on UseEffect

React's useEffect hook is a powerful tool for managing side effects, but it can also be a source of bugs. Be mindful of when to use useEffect with an exhaustive dependency array. If you're not careful, you can end up with an infinite loop of re-renders.

Write Tests

Tests help ensure that your code works as expected and makes it easier to refactor your code with confidence. Use a testing library like Jest or React Testing Library to write unit tests for your components, and consider using a tool like Cypress for end-to-end tests.

Styling Components

I recommend the following:

Styled Components

Styled-Components are particularly useful for styling components in isolation. If you have a situation where a given page has multiple styling libraries, you can confidently use Styled Components without fear of bleed.

Tailwind CSS

TailwindCSS is a utility-first CSS framework that makes it easy to style components. It's particularly useful for reasoning about what a given component does without having to reference a separate file. However, if you're using TailwindCSS, you need to be cautious about using it in conjunction with other styling libraries.

Use a Linter and Formatter

When it comes to Pull Requests, it's important to have a consistent style. With that said, I don't want to see a single comment about formatting in a PR. The best way to avoid this is to use a linter and formatter. If it's made it to a PR, it's already been linted and formatted.

A linter like ESLint can help catch common mistakes and enforce coding standards, while a formatter like Prettier can automatically format your code to ensure a consistent style. Set up your linter and formatter to run on save or before commits to catch and fix issues early.

I recommend using ESLint and Prettier together, and using a tool like Husky to run them automatically on save or before commits.

Optimize Performance

React's virtual DOM helps optimize rendering, but there are still things you can do to improve performance. Use React's useMemo and useCallback hooks to avoid unnecessary computations and re-renders, and use React's lazy and Suspense features to code-split and lazily load components.

Conclusion

Architecting a React TypeScript web application involves careful planning and consideration. By following these good practices, you can build an application that's maintainable, scalable, and efficient. Remember, the key to a successful project is not just writing code, but also designing a solid architecture that can support your application as it grows and evolves.