Mastering React: Best Practices for Building Robust and Scalable Applications

Introduction

In this blog post, we'll explore some essential React Best Practices for both senior and new developers. Let's jump right into the list.

What is React ?

React is a popular JavaScript library for building user interfaces. It provides a component-based architecture that allows for reusable and modular code. With the addition of TypeScript, the development experience can be even better. Here are some best practices for using React with TypeScript.

Organize your code into modules

Breaking your code into smaller, more manageable modules is essential for large-scale projects. This makes it easier to maintain, test, and scale your codebase. Use a consistent naming convention for your files and folders and group related code together.

src/
├── components/
│   ├── Button.tsx
│   ├── Input.tsx
│   ├── ...
├── pages/
│   ├── Home.tsx
│   ├── About.tsx
│   ├── ...
├── utils/
│   ├── api.ts
│   ├── helpers.ts
│   ├── ...
└── App.tsx

Use a state management library

As your project grows, managing the state of your application becomes more complex. Using a state management library like Redux, MobX, or Recoil can help you manage state more efficiently and keep your code organized.

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  count: 0,
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.count += 1;
    },
    decrement: state => {
      state.count -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;

export default counterSlice.reducer;

Use code splitting

Code splitting is a technique that allows you to split your code into smaller, more manageable chunks that can be loaded on demand. This can help reduce the initial load time of your application and improve its performance.

import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Home />
        <About />
      </Suspense>
    </div>
  );
}

export default App;

Use server-side rendering (SSR)

Server-side rendering is a technique that allows you to render your React application on the server and send the HTML to the client. This can help improve the performance of your application, as it reduces the amount of work that the client needs to do.

import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const server = express();

server.get('/', (req, res) => {
  const html = renderToString(<App />);

  res.send(`
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="bundle.js"></script>
      </body>
    </html>
  `);
});

server.listen(3000, () => {
  console.log('Server started on port 3000');
});

Use performance tools

As your application grows, it's essential to monitor its performance and identify any bottlenecks or areas for improvement. Use performance tools like Lighthouse, React Profiler, or Chrome DevTools to identify performance issues and optimize your code.

import React, { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number,
  interactions: Set<string>) {
	console.log([Profiler] ${id} ${phase} (actual time: ${actualDuration} ms));
}

function App() {
	return (
		<Profiler id="App" onRender={onRenderCallback}>
			<div>My App</div>
		</Profiler>
	);
}

export default App;

Use functional components

Functional components are easier to read, test and maintain than class components. They are also faster to render. Here is an example of a functional component with TypeScript.

import React from 'react';

type Props = {
  name: string;
};

const Greeting: React.FC<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

export default Greeting;

Use PropTypes or TypeScript interfaces for type checking

TypeScript provides a way to check types at compile-time, which helps catch errors early in the development process. You can use PropTypes or TypeScript interfaces to define the expected types of props passed to a component. Here is an example of a functional component using PropTypes.

//JavaScript
import React from 'react';
import PropTypes from 'prop-types';

const Greeting = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

Greeting.propTypes = {
  name: PropTypes.string.isRequired,
};

export default Greeting;

And here is an example using TypeScript interfaces.

//TypeScript

import React from 'react';

interface Props {
  name: string;
}

const Greeting: React.FC<Props> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

export default Greeting;

Use React hooks

React hooks provide a way to add state and lifecycle methods to functional components. They are more concise and easier to use than class components. Here is an example of a component using the useState hook.

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

Use CSS modules for styling

CSS modules allow you to write modular and scoped CSS, which makes it easier to manage your styles. Here is an example of a component using CSS modules.

import React from 'react';
import styles from './Button.module.css';

interface Props {
  onClick: () => void;
}

const Button: React.FC<Props> = ({ onClick, children }) => {
  return (
    <button className={styles.button} onClick={onClick}>
      {children}
    </button>
  );
};

export default Button;

And here is an example of a CSS module.

.button {
  color: white;
  background-color: blue;
  border: none;
  border-radius: 4px;
  padding: 8px 16px;
  cursor: pointer;
}

Use TypeScript with strict mode enabled

TypeScript strict mode provides additional type checking and helps catch errors early in the development process. To enable strict mode, you can set "strict": true in your tsconfig.json file.

{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "jsx": "react"
  }
}

Use generics with props

Generics can be used with React props to ensure type safety. This is especially useful when working with components that accept multiple types of props. Here is an example of a component using generics with props.

import React from 'react';

interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: Props<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

export default List;

Use enums for constants

Enums can be used to define constants in your React code. This provides type safety and helps avoid typos or misspelled constants. Here is an example of an enum.

enum Color {
  Red = 'red',
  Green = 'green',
  Blue = 'blue',
}

function App() {
  const color = Color.Red;
  return <div style={{ backgroundColor: color }}>Hello, world!</div>;
}

export default App;

Use defaultProps for default props

You can use defaultProps to provide default values for props in your React components. This makes your code more readable and provides better documentation. Here is an example of a component using defaultProps.

import React from 'react';

interface Props {
  name: string;
  age?: number;
}

function Person({ name, age = 30 }: Props) {
  return (
    <div>
      <p>Name: {name}</p>
      <p>Age: {age}</p>
    </div>
  );
}

Person.defaultProps = {
  age: 30,
};

export default Person;

Use React.memo for performance optimization

React.memo can be used to memoize functional components, which improves performance by preventing unnecessary re-renders. Here is an example of a memoized component.

import React from 'react';

interface Props {
  name: string;
}

const Greeting: React.FC<Props> = React.memo(({ name }) => {
  return <div>Hello, {name}!</div>;
});

export default Greeting;

Use React.useEffect to manage side effects

React.useEffect can be used to manage side effects in your components, such as fetching data from an API or updating the document title. Here is an example of a component using useEffect.

import React, { useEffect, useState } from 'react';

function App() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setData(data);
  };

  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

export default App;

Use interfaces for state

When defining state in your React components, it's a good practice to use interfaces to define the shape of the state object. This provides better type safety and makes it easier to understand the structure of the state. Here's an example.

import React, { useState } from 'react';

interface State {
  count: number;
  text: string;
}

function MyComponent() {
  const [state, setState] = useState<State>({ count: 0, text: '' });

  const handleIncrement = () => {
    setState(prevState => ({ ...prevState, count: prevState.count + 1 }));
  };

  const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setState(prevState => ({ ...prevState, text: event.target.value }));
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={handleIncrement}>Increment</button>
      <input type="text" value={state.text} onChange={handleTextChange} />
    </div>
  );
}

export default MyComponent;

Use type aliases for complex types

When working with complex types in your React components, it's often helpful to use type aliases to simplify the code and make it more readable. Here's an example.

import React from 'react';

type User = {
  id: number;
  name: string;
  email: string;
};

type Props = {
  user: User;
};

function UserProfile({ user }: Props) {
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
}

export default UserProfile;

Use the strictNullChecks compiler option

Enabling the strictNullChecks compiler option in your TypeScript configuration ensures that null and undefined are not assignable to variables unless they are explicitly allowed. This can help catch bugs early on in your code. Here's an example of how to enable this option in your tsconfig.json file.

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Use non-null assertions sparingly

Using non-null assertions (!) in your code should be done sparingly, as it can lead to runtime errors if the value is actually null or undefined. Instead, consider using optional chaining (?.) or nullish coalescing (??) to handle these cases. Here's an example.

import React from 'react';

interface Props {
  user?: {
    id: number;
    name: string;
  };
}

function UserProfile({ user }: Props) {
  const userName = user?.name ?? 'Unknown';

  return (
    <div>
      <p>User ID: {user?.id}</p>
      <p>User Name: {userName}</p>
    </div>
  );
}

export default UserProfile;

Conclusion

Following best practices when building React projects is essential. By using TypeScript, writing automated tests, optimizing for performance, and implementing code reviews, you can create robust and maintainable applications. These practices are crucial for both small and large-scale projects, and they can help ensure your code's quality and reliability.

So, always keep these best practices in mind when developing large-scale or small applications with React

Thank you guys & Happy Coding!!