Skip to main content

UI Best Practices

React Best Practices

  • Use functional components and hooks
    • React has moved towards functional components and hooks instead of class-based components. Using functional components and hooks can help improve performance, reduce complexity, and make the code easier to understand and maintain.
  • Keep components small and reusable
    • Keep React components small and focused on a specific task.
    • This can help improve the overall maintainability of the code and make it easier to test and debug.
  • Use Typescript or PropTypes to validate component props
    • PropTypes is a useful tool for validating the props passed to a component. This helps to catch errors early and makes it easier to identify and fix problems.
  • Avoid using the index as a key
    • When rendering lists in React, it's important to use a unique identifier as the key for each element. Using the index as the key can cause issues with rendering and performance.
  • Use state sparingly and lift state up where necessary
    • Limit the number of stateful components.
      • Where possible, state should be lifted up to a higher-level component to reduce complexity and improve maintainability.
  • Use React.memo to optimize performance
    • React.memo is a higher-order component that can help optimize the rendering of functional components. It caches the result of the component and only re-renders if the props have changed.
  • Use React.lazy and code splitting to optimize performance
    • React.lazy is a way to lazily load components, which can help reduce the initial load time of an application. Code splitting is another technique for reducing the size of the initial bundle.
  • Use CSS modules or CSS-in-JS for styling: Using CSS modules or CSS-in-JS can help avoid conflicts and improve the maintainability of the styling code.
  • Use React testing library for testing
    • The React testing library provides a simple and intuitive API for testing React components. It encourages testing from the perspective of the user and can help catch bugs early.
  • Use React DevTools for debugging:
    • React DevTools is a browser extension that can help with debugging and profiling React applications. It provides a wealth of information about the component tree, state, and props, making it easier to identify and fix issues.
  • React's PureComponent or shouldComponentUpdate to avoid unnecessary renders and optimize performance.
  • React's memo() or React.memo() for functional components to achieve the same performance benefits as PureComponent.
  • React context API to avoid prop drilling and pass down common data or state to multiple components.
  • propTypes to ensure correct props are being passed to the components and prevent runtime errors.
  • Use state management libraries such as Redux or MobX to manage complex state in the application and avoid deeply nested prop drilling.
  • React Router for routing and navigating within the application and avoid hard-coding URLs or using anchors.
  • React hooks to manage state and side-effects in functional components instead of class components to keep code simple and reusable.

React Hooks Best Practices

  • Use hooks instead of class components
    • Hooks allow you to manage state and side effects in functional components, making them more readable, reusable, and easier to test.
  • Follow the rules of hooks
    • Only use hooks at the top level of your component or inside other hooks, and only call hooks from React function components, not regular JavaScript functions.
  • Use useState for state management
    • useState is a built-in hook that allows you to manage state in functional components.
  • Use useEffect for side effects:
    • useEffect is a built-in hook that allows you to manage side effects in functional components, such as fetching data or setting up event listeners.
  • Use useCallback and useMemo to optimize performance:
    • These hooks allow you to optimize performance by memoizing expensive function calls or preventing unnecessary re-renders of components.
  • Use custom hooks to share stateful logic:
    • Custom hooks allow you to share stateful logic between multiple components, making your code more reusable and easier to maintain.
  • Don't use too many hooks in one component:
    • While hooks are great for managing state and side effects, using too many of them in one component can make the code harder to read and maintain. Try to keep the number of hooks in a component to a minimum.
  • Name your hooks properly:
    • When creating custom hooks, make sure to name them with the "use" prefix so that other developers know they are using a hook.
  • Write clean and readable code:
    • Just like with any code, it's important to write clean and readable code when using hooks. This includes using descriptive variable and function names, following best practices for indentation and spacing, and adding comments where necessary.

Most Common React Hooks

  • useState:
    • Add state to your functional components, letting you update the UI based on changes to the state.
  • useEffect:
    • Manage side effects in your functional components, such as fetching data, setting up timers, or subscribing to events.
  • useContext:
    • consume data from a parent component without the need for props drilling.
  • useReducer:
    • An alternative to useState for managing more complex state logic.
  • useCallback:
    • Memoize functions to prevent unnecessary re-renders of components that depend on them.
  • useMemo:
    • Memoize values to prevent unnecessary recalculations when they are not needed.
  • useRef:
    • Create a mutable reference to a DOM element or any other value that persists across renders.
  • useLayoutEffect:
    • Similar to useEffect, but it runs synchronously after the DOM has been updated.
  • useImperativeHandle:
    • Allows you to expose certain functionality of a child component to its parent component.
  • useDebugValue:
    • Allows you to label custom hooks for debugging purposes in React DevTools.

React Typescript Best Practices

  • Use interfaces to define props and state
    • TypeScript's type checking makes it easy to catch errors early and avoid common bugs when defining components. Interfaces help define the shape of the props and state objects and make it clear what types are expected.
  • Use generics to define component types
    • Generics allow you to create reusable components that can work with different data types. This can be especially useful when working with lists and tables.
interface Props<T> {
data: T;
}

function MyComponent<T>(props: Props<T>) {
return <div>{props.data}</div>;
}

const myStringComponent = <MyComponent<string> data="Hello World!" />;
const myNumberComponent = <MyComponent<number> data={42} />;
  • Use the "as" keyword for type assertions
    • When working with external libraries or APIs that don't have TypeScript definitions, you can use the "as" keyword to tell TypeScript that a value is of a certain type.
interface User {
id: number;
name: string;
email: string;
}

const userData: unknown = { id: 1, name: 'John', email: 'john@example.com', isAdmin: true };

const user = userData as User;
  • Use "strictNullChecks" to prevent null and undefined errors

    • Enabling this TypeScript compiler option ensures that you don't accidentally pass null or undefined values to components or functions.
  • Use non-null assertions sparingly

    • Non-null assertions (the "!" operator) can be useful when you know that a value will never be null or undefined, but they can also hide bugs and cause unexpected errors.
  • Use "never" for exhaustive type checking

    • The "never" type can be used to ensure that all possible cases in a switch statement or conditional block are covered.
  • Use enums for constants

    • Enumerations can be used to define constants and improve code readability. They can also help catch errors early by ensuring that the correct values are passed to components and functions.
  • Use utility types to avoid repetition

    • TypeScript includes several utility types, such as "Partial", "Readonly", and "Record", that can help avoid repeating the same type definitions over and over.
  • Partial - Makes all properties in an interface optional.

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

type PartialUser = Partial<User>;

const user: PartialUser = { name: 'John' };

  • Read-only - Makes all properties in an interface read-only.
interface User {
readonly id: number;
name: string;
email: string;
}

type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = { id: 1, name: 'John', email: 'john@example.com' };
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
  • Pick - create a new type that contains only a subset of the properties from the original type.
interface User {
id: number;
name: string;
email: string;
}

type UserPreview = Pick<User, 'id' | 'name'>;

const userPreview: UserPreview = { id: 1, name: 'John' };

  • Omit - create a new type that contains all properties from the original type except for the ones specified.
interface User {
id: number;
name: string;
email: string;
}

type UserPreview = Omit<User, 'email'>;

const userPreview: UserPreview = { id: 1, name: 'John' };

  • Record - create a new type that maps keys to a specific type.
interface User {
id: number;
name: string;
email: string;
}

type UsersById = Record<number, User>;

const usersById: UsersById = {
1: { id: 1, name: 'John', email: 'john@example.com' },
2: { id: 2, name: 'Jane', email: 'jane@example.com' }
};

  • Use the "keyof" operator to ensure type safety
    • The "keyof" operator can be used to ensure that a property name is valid and exists on an object. This can be especially useful when working with dynamic data.
    • It creates a union type of all property keys of a specific type.
interface User {
id: number;
name: string;
email: string;
}

interface UserDetailsProps {
user: User;
displayField: keyof User;
}

const UserDetails: React.FC<UserDetailsProps> = ({ user, displayField }) => {
return (
<div>
<p>
{displayField}: {user[displayField]}
</p>
</div>
);
};

export default UserDetails;

  • Use type guards for type safety
    • Type guards are functions that return a boolean value to check whether a value is of a certain type.
    • They can be used to ensure type safety when working with complex data structures.
import React from 'react';

type Animal = {
type: 'dog' | 'cat';
name: string;
};

type Car = {
type: 'car';
brand: string;
};

type ListItem = Animal | Car;

interface ListItemProps {
item: ListItem;
}

const isAnimal = (item: ListItem): item is Animal => {
return item.type === 'dog' || item.type === 'cat';
};

const ListItemComponent: React.FC<ListItemProps> = ({ item }) => {
if (isAnimal(item)) {
return <div>{`${item.name} is a ${item.type}`}</div>;
} else {
return <div>{`${item.brand} is a ${item.type}`}</div>;
}
};

export default ListItemComponent;

  • Use strict mode
    • TypeScript comes with strict mode, which can help catch potential errors early in the development process. Enabling strict mode can help enforce better coding practices and prevent errors from slipping through.
  • Use enums for constants
    • Instead of using string literals or constants, using TypeScript enums can help avoid typos and make code more maintainable.
  • Use generics to write reusable code
    • TypeScript's support for generics can help write reusable code and prevent duplication.
  • Use union types for props
    • When a component can accept multiple types for a prop, using a union type can help catch potential errors earlier and make the code more maintainable.
  • Map types
    • Create new types based on existing ones by mapping their properties through a transformation.
type Partial<T> = {
[P in keyof T]?: T[P];
};

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

type OptionalUser = Partial<User>;

  • Conditional types
    • Enable complex relationships between types. They allow you to choose one type or another based on a condition using the ternary operator.
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
'unknown';

type StringType = TypeName<string>; // 'string'
type NumberType = TypeName<number>; // 'number'

  • Discriminated Unions
    • Also known as tagged unions, help you create a common type that represents a closed set of related types.
interface Circle {
kind: 'circle';
radius: number;
}

interface Square {
kind: 'square';
sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
}
}