Skip to content

piotrwitek/react-redux-typescript-guide

Repository files navigation

React & Redux in TypeScript - Complete Guide

"This guide is a living compendium documenting the most important patterns and recipes on how to use React (and its Ecosystem) in a functional style using TypeScript. It will help you make your code completely type-safe while focusing on inferring the types from implementation so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."

Join the community on Spectrum Join the chat at https://gitter.im/react-redux-typescript-guide/Lobby

Found it useful? Want more updates?

Show your support by giving a ⭐

Buy Me a Coffee Become a Patron



What's new?

🎉 Now updated to support TypeScript v4.6 🎉 🚀 _Updated to typesafe-actions@5.x 🚀



Goals

  • Complete type safety (with --strict flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking with any type)
  • Make type annotations concise by eliminating redundancy in types using advanced TypeScript Language features like Type Inference and Control flow analysis
  • Reduce repetition and complexity of types with TypeScript focused complementary libraries

React, Redux, Typescript Ecosystem

  • typesafe-actions - Typesafe utilities for "action-creators" in Redux / Flux Architecture
  • utility-types - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types.
  • react-redux-typescript-scripts - dev-tools configuration files shared between projects based on this guide

Examples

Playground Project

Build Status

Check out our Playground Project located in the /playground folder. It contains all source files of the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type-definitions (like @types/react or @types/react-redux) to ensure the examples are up-to-date and not broken with updated definitions (It's based on create-react-app --typescript).

Playground project was created so that you can simply clone the repository locally and immediately play around with all the component patterns found in the guide. It will help you to learn all the examples from this guide in a real project environment without the need to create complicated environment setup by yourself.

Contributing Guide

You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: CONTRIBUTING.md

Funding

You can also help by funding issues. Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.

I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.

Let's fund issues in this repository


🌟 - New or updated section

Table of Contents


Installation

Types for React & Redux

npm i -D @types/react @types/react-dom @types/react-redux

"react" - @types/react
"react-dom" - @types/react-dom
"redux" - (types included with npm package)*
"react-redux" - @types/react-redux

*NB: Guide is based on types for Redux >= v4.x.x.

⇧ back to top


React Types Cheatsheet

React.FC<Props> | React.FunctionComponent<Props>

Type representing a functional component

const MyComponent: React.FC<Props> = ...

React.Component<Props, State>

Type representing a class component

class MyComponent extends React.Component<Props, State> { ...

React.ComponentType<Props>

Type representing union of (React.FC<Props> | React.Component<Props>) - used in HOC

const withState = <P extends WrappedComponentProps>(
  WrappedComponent: React.ComponentType<P>,
) => { ...

React.ComponentProps<typeof XXX>

Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props)

type MyComponentProps = React.ComponentProps<typeof MyComponent>;

React.ReactElement | JSX.Element

Type representing a concept of React Element - representation of a native DOM component (e.g. <div />), or a user-defined composite component (e.g. <MyComponent />)

const elementOnly: React.ReactElement = <div /> || <MyComponent />;

React.ReactNode

Type representing any possible type of React node (basically ReactElement (including Fragments and Portals) + primitive JS types)

const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;
const Component = ({ children: React.ReactNode }) => ...

React.CSSProperties

Type representing style object in JSX - for css-in-js styles

const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...

React.XXXHTMLAttributes<HTMLXXXElement>

Type representing HTML attributes of specified HTML Element - for extending HTML Elements

const Input: React.FC<Props & React.InputHTMLAttributes<HTMLInputElement>> = props => { ... }

<Input about={...} accept={...} alt={...} ... />

React.ReactEventHandler<HTMLXXXElement>

Type representing generic event handler - for declaring event handlers

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } 

<input onChange={handleChange} ... />

React.XXXEvent<HTMLXXXElement>

Type representing more specific event. Some common event examples: ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent.

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

In code above React.MouseEvent<HTMLDivElement> is type of mouse event, and this event happened on HTMLDivElement

⇧ back to top


React

Function Components - FC

- Counter Component

import * as React from 'react';

type Props = {
  label: string;
  count: number;
  onIncrement: () => void;
};

export const FCCounter: React.FC<Props> = props => {
  const { label, count, onIncrement } = props;

  const handleIncrement = () => {
    onIncrement();
  };

  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};

⟩⟩⟩ demo

⇧ back to top

- Counter Component with default props

import * as React from 'react';

type Props = {
  label: string;
  count: number;
  onIncrement: () => void;
};

// React.FC is unaplicable here due not working properly with default props
// https://github.com/facebook/create-react-app/pull/8177
export const FCCounterWithDefaultProps = (props: Props): JSX.Element => {
  const { label, count, onIncrement } = props;

  const handleIncrement = () => {
    onIncrement();
  };

  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};

FCCounterWithDefaultProps.defaultProps = { count: 5 };

⟩⟩⟩ demo

⇧ back to top

- Spreading attributes in Component

import * as React from 'react';

type Props = React.PropsWithChildren<{
  className?: string;
  style?: React.CSSProperties;
}>;

export const FCSpreadAttributes: React.FC<Props> = (props) => {
  const { children, ...restProps } = props;

  return <div {...restProps}>{children}</div>;
};

⟩⟩⟩ demo

⇧ back to top


Class Components

- Class Counter Component

import * as React from 'react';

type Props = {
  label: string;
};

type State = {
  count: number;
};

export class ClassCounter extends React.Component<Props, State> {
  readonly state: State = {
    count: 0,
  };

  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;

    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ back to top

- Class Component with default props

import * as React from 'react';

type Props = {
  label: string;
  initialCount: number;
};

type State = {
  count: number;
};

export class ClassCounterWithDefaultProps extends React.Component<
  Props,
  State
> {
  static defaultProps = {
    initialCount: 0,
  };

  readonly state: State = {
    count: this.props.initialCount,
  };

  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    const { handleIncrement } = this;
    const { label } = this.props;
    const { count } = this.state;

    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ back to top


Generic Components

  • easily create typed component variations and reuse common logic
  • common use case is a generic list components

- Generic List Component

import * as React from 'react';

export interface GenericListProps<T> {
  items: T[];
  itemRenderer: (item: T) => JSX.Element;
}

export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {
  render() {
    const { items, itemRenderer } = this.props;

    return (
      <div>
        {items.map(itemRenderer)}
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ back to top


Hooks

https://reactjs.org/docs/hooks-intro.html

- useState

https://reactjs.org/docs/hooks-reference.html#usestate

import * as React from 'react';

type Props = { initialCount: number };

export default function Counter({initialCount}: Props) {
  const [count, setCount] = React.useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

⇧ back to top

- useContext

https://reactjs.org/docs/hooks-reference.html#usecontext

import * as React from 'react';
import ThemeContext from '../context/theme-context';

type Props = {};

export default function ThemeToggleButton(props: Props) {
  const { theme, toggleTheme } = React.useContext(ThemeContext);
  return (
    <button onClick={toggleTheme} style={theme} >
      Toggle Theme
    </button>
  );
}

⇧ back to top

- useReducer

https://reactjs.org/docs/hooks-reference.html#usereducer

import * as React from 'react';

interface State {
  count: number;
}

type Action = { type: 'reset' } | { 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 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}

interface CounterProps {
  initialCount: number;
}

function Counter({ initialCount }: CounterProps) {
  const [state, dispatch] = React.useReducer(reducer, {
    count: initialCount,
  });

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

export default Counter;

⇧ back to top


Render Props

https://reactjs.org/docs/render-props.html

- Name Provider Component

Simple component using children as a render prop

import * as React from 'react';

interface NameProviderProps {
  children: (state: NameProviderState) => React.ReactNode;
}

interface NameProviderState {
  readonly name: string;
}

export class NameProvider extends React.Component<NameProviderProps, NameProviderState> {
  readonly state: NameProviderState = { name: 'Piotr' };

  render() {
    return this.props.children(this.state);
  }
}

⟩⟩⟩ demo

⇧ back to top

- Mouse Provider Component

Mouse component found in Render Props React Docs

import * as React from 'react';

export interface MouseProviderProps {
  render: (state: MouseProviderState) => React.ReactNode;
}

interface MouseProviderState {
  readonly x: number;
  readonly y: number;
}

export class MouseProvider extends React.Component<MouseProviderProps, MouseProviderState> {
  readonly state: MouseProviderState = { x: 0, y: 0 };

  handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

⟩⟩⟩ demo

⇧ back to top


Higher-Order Components

https://reactjs.org/docs/higher-order-components.html

- HOC wrapping a component

Adds state to a stateless counter

import React from 'react';
import { Diff } from 'utility-types';

// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () => void;
}

export const withState = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  type HocProps = Diff<BaseProps, InjectedProps> & {
    // here you can extend hoc with new props
    initialCount?: number;
  };
  type HocState = {
    readonly count: number;
  };

  return class Hoc extends React.Component<HocProps, HocState> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withState(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    readonly state: HocState = {
      count: Number(this.props.initialCount) || 0,
    };

    handleIncrement = () => {
      this.setState({ count: this.state.count + 1 });
    };

    render() {
      const { ...restProps } = this.props;
      const { count } = this.state;

      return (
        <BaseComponent
        {...(restProps as BaseProps)}
          count={count} // injected
          onIncrement={this.handleIncrement} // injected
        />
      );
    }
  };
};
Click to expand

import * as React from 'react';

import { withState } from '../hoc';
import { FCCounter } from '../components';

const FCCounterWithState = withState(FCCounter);

export default () => <FCCounterWithState label={'FCCounterWithState'} />;

⇧ back to top

- HOC wrapping a component and injecting props

Adds error handling using componentDidCatch to any component

import React from 'react';

const MISSING_ERROR = 'Error was swallowed during propagation.';

export const withErrorBoundary = <BaseProps extends {}>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  type HocProps = React.PropsWithChildren<{
    // here you can extend hoc with new props
  }>;
  type HocState = {
    readonly error: Error | null | undefined;
  };

  return class Hoc extends React.Component<HocProps, HocState> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withErrorBoundary(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    readonly state: HocState = {
      error: undefined,
    };

    componentDidCatch(error: Error | null, info: object) {
      this.setState({ error: error || new Error(MISSING_ERROR) });
      this.logErrorToCloud(error, info);
    }

    logErrorToCloud = (error: Error | null, info: object) => {
      // TODO: send error report to service provider
    };

    render() {
      const { children, ...restProps } = this.props;
      const { error } = this.state;

      if (error) {
        return <BaseComponent {...(restProps as BaseProps)} />;
      }

      return children;
    }
  };
};
Click to expand

import React, {useState} from 'react';

import { withErrorBoundary } from '../hoc';
import { ErrorMessage } from '../components';

const ErrorMessageWithErrorBoundary =
  withErrorBoundary(ErrorMessage);

const BrokenComponent = () => {
  throw new Error('I\'m broken! Don\'t render me.');
};

const BrokenButton = () => {
  const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] =
    useState(false);

  if (shouldRenderBrokenComponent) {
    return <BrokenComponent />;
  }

  return (
    <button
      type="button"
      onClick={() => {
        setShouldRenderBrokenComponent(true);
      }}
    >
      {`Throw nasty error`}
    </button>
  );
};

export default () => (
  <ErrorMessageWithErrorBoundary>
    <BrokenButton />
  </ErrorMessageWithErrorBoundary>
);

⇧ back to top

- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟

Adds error handling using componentDidCatch to any component

import { RootState } from 'MyTypes';
import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';
import { countersActions, countersSelectors } from '../features/counters';

// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () => void;
}

export const withConnectedCount = <BaseProps extends InjectedProps>(
  BaseComponent: React.ComponentType<BaseProps>
) => {
  const mapStateToProps = (state: RootState) => ({
    count: countersSelectors.getReduxCounter(state.counters),
  });

  const dispatchProps = {
    onIncrement: countersActions.increment,
  };

  type HocProps = ReturnType<typeof mapStateToProps> &
    typeof dispatchProps & {
      // here you can extend ConnectedHoc with new props
      overrideCount?: number;
    };

  class Hoc extends React.Component<HocProps> {
    // Enhance component name for debugging and React-Dev-Tools
    static displayName = `withConnectedCount(${BaseComponent.name})`;
    // reference to original wrapped component
    static readonly WrappedComponent = BaseComponent;

    render() {
      const { count, onIncrement, overrideCount, ...restProps } = this.props;

      return (
        <BaseComponent
          {...(restProps as BaseProps)}
          count={overrideCount || count} // injected
          onIncrement={onIncrement} // injected
        />
      );
    }
  }

  const ConnectedHoc = connect<
    ReturnType<typeof mapStateToProps>,
    typeof dispatchProps, // use "undefined" if NOT using dispatchProps
    Diff<BaseProps, InjectedProps>,
    RootState
  >(
    mapStateToProps,
    dispatchProps
  )(Hoc);

  return ConnectedHoc;
};
Click to expand

import * as React from 'react';

import { withConnectedCount } from '../hoc';
import { FCCounter } from '../components';

const FCCounterWithConnectedCount = withConnectedCount(FCCounter);

export default () => (
  <FCCounterWithConnectedCount overrideCount={5} label={'FCCounterWithState'} />
);

⇧ back to top


Redux Connected Components

- Redux connected counter

import Types from 'MyTypes';
import { connect } from 'react-redux';

import { countersActions, countersSelectors } from '../features/counters';
import { FCCounter } from '../components';

const mapStateToProps = (state: Types.RootState) => ({
  count: countersSelectors.getReduxCounter(state.counters),
});

const dispatchProps = {
  onIncrement: countersActions.increment,
};

export const FCCounterConnected = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
Click to expand

import * as React from 'react';

import { FCCounterConnected } from '.';

export default () => <FCCounterConnected label={'FCCounterConnected'} />;

⇧ back to top

- Redux connected counter with own props

import Types from 'MyTypes';
import { connect } from 'react-redux';

import { countersActions, countersSelectors } from '../features/counters';
import { FCCounter } from '../components';

type OwnProps = {
  initialCount?: number;
};

const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({
  count:
    countersSelectors.getReduxCounter(state.counters) +
    (ownProps.initialCount || 0),
});

const dispatchProps = {
  onIncrement: countersActions.increment,
};

export const FCCounterConnectedOwnProps = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
Click to expand

import * as React from 'react';

import { FCCounterConnectedOwnProps } from '.';

export default () => (
  <FCCounterConnectedOwnProps
    label={'FCCounterConnectedOwnProps'}
    initialCount={10}
  />
);

⇧ back to top

- Redux connected counter via hooks

import * as React from 'react';
import { FCCounter } from '../components';
import { increment } from '../features/counters/actions';
import { useSelector, useDispatch } from '../store/hooks';

const FCCounterConnectedHooksUsage: React.FC = () => {
  const counter = useSelector(state => state.counters.reduxCounter);
  const dispatch = useDispatch();
  return <FCCounter label="Use selector" count={counter} onIncrement={() => dispatch(increment())}/>;
};

export default FCCounterConnectedHooksUsage;

⇧ back to top

- Redux connected counter with redux-thunk integration

import Types from 'MyTypes';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import * as React from 'react';

import { countersActions } from '../features/counters';

// Thunk Action
const incrementWithDelay = () => async (dispatch: Dispatch): Promise<void> => {
  setTimeout(() => dispatch(countersActions.increment()), 1000);
};

const mapStateToProps = (state: Types.RootState) => ({
  count: state.counters.reduxCounter,
});

const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
  bindActionCreators(
    {
      onIncrement: incrementWithDelay,
    },
    dispatch
  );

type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps> & {
    label: string;
  };

export const FCCounter: React.FC<Props> = props => {
  const { label, count, onIncrement } = props;

  const handleIncrement = () => {
    // Thunk action is correctly typed as promise
    onIncrement().then(() => {
      // ...
    });
  };

  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};

export const FCCounterConnectedBindActionCreators = connect(
  mapStateToProps,
  mapDispatchToProps
)(FCCounter);
Click to expand

import * as React from 'react';

import { FCCounterConnectedBindActionCreators } from '.';

export default () => (
  <FCCounterConnectedBindActionCreators
    label={'FCCounterConnectedBindActionCreators'}
  />
);

⇧ back to top

Context

https://reactjs.org/docs/context.html

ThemeContext

import * as React from 'react';

export type Theme = React.CSSProperties;

type Themes = {
  dark: Theme;
  light: Theme;
};

export const themes: Themes = {
  dark: {
    color: 'black',
    backgroundColor: 'white',
  },
  light: {
    color: 'white',
    backgroundColor: 'black',
  },
};

export type ThemeContextProps = { theme: Theme; toggleTheme?: () => void };
const ThemeContext = React.createContext<ThemeContextProps>({ theme: themes.light });

export default ThemeContext;

⇧ back to top

ThemeProvider

import React from 'react';
import ThemeContext, { themes, Theme } from './theme-context';
import ToggleThemeButton from './theme-consumer';

interface State {
  theme: Theme;
}
export class ThemeProvider extends React.Component<{}, State> {
  readonly state: State = { theme: themes.light };

  toggleTheme = () => {
    this.setState(state => ({
      theme: state.theme === themes.light ? themes.dark : themes.light,
    }));
  }

  render() {
    const { theme } = this.state;
    const { toggleTheme } = this;
    return (
      <ThemeContext.Provider value={{ theme, toggleTheme }}>
        <ToggleThemeButton />
      </ThemeContext.Provider>
    );
  }
}

⇧ back to top

ThemeConsumer

import * as React from 'react';
import ThemeContext from './theme-context';

type Props = {};

export default function ToggleThemeButton(props: Props) {
  return (
    <ThemeContext.Consumer>
      {({ theme, toggleTheme }) => <button style={theme} onClick={toggleTheme} {...props} />}
    </ThemeContext.Consumer>
  );
}

ThemeConsumer in class component

import * as React from 'react';
import ThemeContext from './theme-context';

type Props = {};

export class ToggleThemeButtonClass extends React.Component<Props> {
  static contextType = ThemeContext;
  declare context: React.ContextType<typeof ThemeContext>;

  render() {
    const { theme, toggleTheme } = this.context;
    return (
      <button style={theme} onClick={toggleTheme}>
        Toggle Theme
      </button>
    );
  }
}

Implementation with Hooks

⇧ back to top


Redux

Store Configuration