Introduction

Building on our previous exploration of basic React components with TypeScript, this post delves into more advanced techniques. Learn to create more complex component patterns and master state management to elevate your React applications.

Using Higher-Order Components

Higher-order components (HOCs) are functions that take a component and return a new component. They’re ideal for reusing component logic. Here’s how you can create a simple HOC that adds additional props to a component:

import React from 'react';

type WithExtraInfoProps = {
  extraInfo: string;
};

function withExtraInfo<T>(WrappedComponent: React.ComponentType<T>) {
  return (props: T & WithExtraInfoProps) => (
    <WrappedComponent {...props} />
  );
}

// Usage
const EnhancedComponent = withExtraInfo(MyComponent);

Utilizing Render Props

Render props refer to a technique for sharing code between React components using a prop whose value is a function.

import React, { useState } from 'react';

type RenderProps = {
  count: number;
  increment: () => void;
};

const Counter: React.FC<{ render: (props: RenderProps) => JSX.Element }> = ({ render }) => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  return render({ count, increment });
};

// Usage
<Counter render={({ count, increment }) => (
  <div>
    <p>{count}</p>
    <button onClick={increment}>Increment</button>
  </div>
)} />

Managing Complex State with UseReducer

For complex state logic, useReducer is a more robust alternative to useState. It’s especially useful for handling multiple sub-values or when the next state depends on the previous one.

import React, { useReducer } from 'react';

type State = {
  count: number;
};

type Action = { type: 'increment' } | { type: 'decrement' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

const Counter: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
};

Using Context API with TypeScript

The Context API is a powerful feature in React for passing data through the component tree without having to pass props down manually at every level. Combined with TypeScript, it ensures type safety across the context.

import React, { useContext, createContext, useState } from 'react';

type UserContextType = {
  user: string;
  setUser: React.Dispatch<React.SetStateAction<string>>;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

const UserProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState('Guest');
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
};

const useUser = () => {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
};

// Usage
const UserProfile: React.FC = () => {
  const { user, setUser } = useUser();
  return <div>Username: {user}</div>;
};

Using Custom Hooks with TypeScript

Custom hooks in React allow you to extract component logic into reusable functions. TypeScript enhances these hooks by adding types to hooks' parameters and return values, making them more robust and easy to maintain.

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

function useWindowSize() {
  const [size, setSize] = useState<{ width: number; height: number }>({ width: window.innerWidth, height: window.innerHeight });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// Usage
const App: React.FC = () => {
  const { width, height } = useWindowSize();
  return <div>Window size: {width} x {height}</div>;
};

Conclusion

These advanced techniques in React and TypeScript not only enhance the flexibility and reusability of your components but also improve the manageability of state in larger applications. Continue exploring these patterns to build scalable and maintainable applications.