Giter Site home page Giter Site logo

piotrwitek / react-redux-typescript-guide Goto Github PK

View Code? Open in Web Editor NEW
13.3K 217.0 1.1K 2.87 MB

The complete guide to static typing in "React & Redux" apps using TypeScript

Home Page: https://piotrwitek.github.io/react-redux-typescript-guide/

License: MIT License

TypeScript 87.81% HTML 3.09% Shell 0.45% JavaScript 6.61% CSS 2.05%
react redux typescript patterns guidelines flow static-typing style-guide guide

react-redux-typescript-guide's Introduction

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 [email protected] 🚀



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

Create Global Store Types

RootState - type representing root state-tree

Can be imported in connected components to provide type-safety to Redux connect function

RootAction - type representing union type of all action objects

Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics

import { StateType, ActionType } from 'typesafe-actions';

declare module 'MyTypes' {
  export type Store = StateType<typeof import('./store').default>;
  export type RootAction = ActionType<typeof import('./root-action').default>;
  export type RootState = StateType<ReturnType<typeof import('./root-reducer').default>>;
}

declare module 'typesafe-actions' {
  interface Types {
    RootAction: ActionType<typeof import('./root-action').default>;
  }
}

⇧ back to top

Create Store

When creating a store instance we don't need to provide any additional types. It will set-up a type-safe Store instance using type inference.

The resulting store instance methods like getState or dispatch will be type checked and will expose all type errors

import { RootAction, RootState, Services } from 'MyTypes';
import { applyMiddleware, createStore } from 'redux';
import { createEpicMiddleware } from 'redux-observable';

import services from '../services';
import { routerMiddleware } from './redux-router';
import rootEpic from './root-epic';
import rootReducer from './root-reducer';
import { composeEnhancers } from './utils';

const epicMiddleware = createEpicMiddleware<
  RootAction,
  RootAction,
  RootState,
  Services
>({
  dependencies: services,
});

// configure middlewares
const middlewares = [epicMiddleware, routerMiddleware];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));

// rehydrate state on app start
const initialState = {};

// create store
const store = createStore(
  rootReducer,
  initialState,
  enhancer
);

epicMiddleware.run(rootEpic);

// export store singleton instance
export default store;

Action Creators 🌟

We'll be using a battle-tested helper library typesafe-actions Latest Stable Version NPM Downloads that's designed to make it easy and fun working with Redux in TypeScript.

To learn more please check this in-depth tutorial: Typesafe-Actions - Tutorial!

A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions.

/* eslint-disable */
import { action } from 'typesafe-actions';

import { ADD, INCREMENT } from './constants';

/* SIMPLE API */

export const increment = () => action(INCREMENT);
export const add = (amount: number) => action(ADD, amount);

/* ADVANCED API */

// More flexible allowing to create complex actions more easily
// use can use "action-creator" instance in place of "type constant"
// e.g. case getType(increment): return action.payload;
// This will allow to completely eliminate need for "constants" in your application, more info here:
// https://github.com/piotrwitek/typesafe-actions#constants

import { createAction } from 'typesafe-actions';
import { Todo } from '../todos/models';

export const emptyAction = createAction(INCREMENT)<void>();
export const payloadAction = createAction(ADD)<number>();
export const payloadMetaAction = createAction(ADD)<number, string>();

export const payloadCreatorAction = createAction(
  'TOGGLE_TODO',
  (todo: Todo) => todo.id
)<string>();
Click to expand

import { store } from '../../store/';
import { countersActions as counter } from '../counters';

// store.dispatch(counter.increment(1)); // Error: Expected 0 arguments, but got 1.
store.dispatch(counter.increment()); // OK

// store.dispatch(counter.add()); // Error: Expected 1 arguments, but got 0.
store.dispatch(counter.add(1)); // OK

⇧ back to top


Reducers

State with Type-level Immutability

Declare reducer State type with readonly modifier to get compile time immutability

export type State = {
  readonly counter: number;
  readonly todos: ReadonlyArray<string>;
};

Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors

export const initialState: State = {
  counter: 0,
}; // OK

initialState.counter = 3; // TS Error: cannot be mutated

It's great for Arrays in JS because it will error when using mutator methods like (push, pop, splice, ...), but it'll still allow immutable methods like (concat, map, slice,...).

state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray<string>'
const newTodos = state.todos.concat('Learn about tagged union types') // OK

Caveat - Readonly is not recursive

This means that the readonly modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly.

TIP: use Readonly or ReadonlyArray Mapped types

export type State = Readonly<{
  counterPairs: ReadonlyArray<Readonly<{
    immutableCounter1: number,
    immutableCounter2: number,
  }>>,
}>;

state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated

Solution - recursive Readonly is called DeepReadonly

To fix this we can use DeepReadonly type (available from utility-types).

import { DeepReadonly } from 'utility-types';

export type State = DeepReadonly<{
  containerObject: {
    innerValue: number,
    numbers: number[],
  }
}>;

state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated
state.containerObject.innerValue = 1; // TS Error: cannot be mutated
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods

⇧ back to top

Typing reducer

to understand following section make sure to learn about Type Inference, Control flow analysis and Tagged union types

import { combineReducers } from 'redux';
import { ActionType } from 'typesafe-actions';

import { Todo, TodosFilter } from './models';
import * as actions from './actions';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';

export type TodosAction = ActionType<typeof actions>;

export type TodosState = Readonly<{
  todos: Todo[];
  todosFilter: TodosFilter;
}>;
const initialState: TodosState = {
  todos: [],
  todosFilter: TodosFilter.All,
};

export default combineReducers<TodosState, TodosAction>({
  todos: (state = initialState.todos, action) => {
    switch (action.type) {
      case ADD:
        return [...state, action.payload];

      case TOGGLE:
        return state.map(item =>
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        );

      default:
        return state;
    }
  },
  todosFilter: (state = initialState.todosFilter, action) => {
    switch (action.type) {
      case CHANGE_FILTER:
        return action.payload;

      default:
        return state;
    }
  },
});

⇧ back to top

Typing reducer with typesafe-actions

Notice we are not required to use any generic type parameter in the API. Try to compare it with regular reducer as they are equivalent.

import { combineReducers } from 'redux';
import { createReducer } from 'typesafe-actions';

import { Todo, TodosFilter } from './models';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';

export type TodosState = Readonly<{
  todos: Todo[];
  todosFilter: TodosFilter;
}>;
const initialState: TodosState = {
  todos: [],
  todosFilter: TodosFilter.All,
};

const todos = createReducer(initialState.todos)
  .handleType(ADD, (state, action) => [...state, action.payload])
  .handleType(TOGGLE, (state, action) =>
    state.map(item =>
      item.id === action.payload
        ? { ...item, completed: !item.completed }
        : item
    )
  );

const todosFilter = createReducer(initialState.todosFilter).handleType(
  CHANGE_FILTER,
  (state, action) => action.payload
);

export default combineReducers({
  todos,
  todosFilter,
});

⇧ back to top

Testing reducer

import {
  todosReducer as reducer,
  todosActions as actions,
} from './';
import { TodosState } from './reducer';

/**
 * FIXTURES
 */
const getInitialState = (initial?: Partial<TodosState>) =>
  reducer(initial as TodosState, {} as any);

/**
 * STORIES
 */
describe('Todos Stories', () => {
  describe('initial state', () => {
    it('should match a snapshot', () => {
      const initialState = getInitialState();
      expect(initialState).toMatchSnapshot();
    });
  });

  describe('adding todos', () => {
    it('should add a new todo as the first element', () => {
      const initialState = getInitialState();
      expect(initialState.todos).toHaveLength(0);
      const state = reducer(initialState, actions.add('new todo'));
      expect(state.todos).toHaveLength(1);
      expect(state.todos[0].title).toEqual('new todo');
    });
  });

  describe('toggling completion state', () => {
    it('should mark active todo as complete', () => {
      const activeTodo = { id: '1', completed: false, title: 'active todo' };
      const initialState = getInitialState({ todos: [activeTodo] });
      expect(initialState.todos[0].completed).toBeFalsy();
      const state1 = reducer(initialState, actions.toggle(activeTodo.id));
      expect(state1.todos[0].completed).toBeTruthy();
    });
  });
});

⇧ back to top


Async Flow with redux-observable

Typing epics

import { RootAction, RootState, Services } from 'MyTypes';
import { Epic } from 'redux-observable';
import { tap, ignoreElements, filter } from 'rxjs/operators';
import { isOfType } from 'typesafe-actions';

import { todosConstants } from '../todos';

// contrived example!!!
export const logAddAction: Epic<RootAction, RootAction, RootState, Services> = (
  action$,
  state$,
  { logger }
) =>
  action$.pipe(
    filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; }
    tap(action => {
      logger.log(
        `action type must be equal: ${todosConstants.ADD} === ${action.type}`
      );
    }),
    ignoreElements()
  );

⇧ back to top

Testing epics

import { StateObservable, ActionsObservable } from 'redux-observable';
import { RootState, RootAction } from 'MyTypes';
import { Subject } from 'rxjs';

import { add } from './actions';
import { logAddAction } from './epics';

// Simple typesafe mock of all the services, you dont't need to mock anything else
// It is decoupled and reusable for all your tests, just put it in a separate file
const services = {
  logger: {
    log: jest.fn(),
  },
  localStorage: {
    loadState: jest.fn(),
    saveState: jest.fn(),
  },
};

describe('Todos Epics', () => {
  let state$: StateObservable<RootState>;

  beforeEach(() => {
    state$ = new StateObservable<RootState>(
      new Subject<RootState>(),
      undefined as any
    );
  });

  describe('logging todos actions', () => {
    beforeEach(() => {
      services.logger.log.mockClear();
    });

    it('should call the logger service when adding a new todo', done => {
      const addTodoAction = add('new todo');
      const action$ = ActionsObservable.of(addTodoAction);

      logAddAction(action$, state$, services)
        .toPromise()
        .then((outputAction: RootAction) => {
          expect(services.logger.log).toHaveBeenCalledTimes(1);
          expect(services.logger.log).toHaveBeenCalledWith(
            'action type must be equal: todos/ADD === todos/ADD'
          );
          // expect output undefined because we're using "ignoreElements" in epic
          expect(outputAction).toEqual(undefined);
          done();
        });
    });
  });
});

⇧ back to top


Selectors with reselect

import { createSelector } from 'reselect';

import { TodosState } from './reducer';

export const getTodos = (state: TodosState) => state.todos;

export const getTodosFilter = (state: TodosState) => state.todosFilter;

export const getFilteredTodos = createSelector(getTodos, getTodosFilter, (todos, todosFilter) => {
  switch (todosFilter) {
    case 'completed':
      return todos.filter(t => t.completed);
    case 'active':
      return todos.filter(t => !t.completed);

    default:
      return todos;
  }
});

⇧ back to top


Connect with react-redux

Typing connected component

NOTE: Below you'll find a short explanation of concepts behind using connect with TypeScript. For more detailed examples please check Redux Connected Components section.

import MyTypes from 'MyTypes';

import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux';
import { connect } from 'react-redux';

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

// Type annotation for "state" argument is mandatory to check 
// the correct shape of state object and injected props you can also
// extend connected component Props interface by annotating `ownProps` argument
const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({
  count: state.counters.reduxCounter,
});

// "dispatch" argument needs an annotation to check the correct shape
//  of an action object when using dispatch function
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators({
    onIncrement: countersActions.increment,
  }, dispatch);

// shorter alternative is to use an object instead of mapDispatchToProps function
const dispatchToProps = {
    onIncrement: countersActions.increment,
};

// Notice we don't need to pass any generic type parameters to neither
// the connect function below nor map functions declared above
// because type inference will infer types from arguments annotations automatically
// This is much cleaner and idiomatic approach
export const FCCounterConnected =
  connect(mapStateToProps, mapDispatchToProps)(FCCounter);

// You can add extra layer of validation of your action creators
// by using bindActionCreators generic type parameter and RootAction type
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators<ActionCreatorsMapObject<Types.RootAction>>({
    invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... }
  }, dispatch);

⇧ back to top

Typing useSelector and useDispatch

import { Dispatch } from 'redux';
import {
  TypedUseSelectorHook,
  useSelector as useGenericSelector,
  useDispatch as useGenericDispatch
} from 'react-redux';
import { RootState, RootAction } from 'MyTypes';

export const useSelector: TypedUseSelectorHook<RootState> = useGenericSelector;

export const useDispatch: () => Dispatch<RootAction> = useGenericDispatch;

⇧ back to top

Typing connected component with redux-thunk integration

NOTE: When using thunk action creators you need to use bindActionCreators. Only this way you can get corrected dispatch props type signature like below.*

WARNING: As of now (Apr 2019) bindActionCreators signature of the latest redux-thunk release will not work as below, you need to use our modified type definitions that you can find here /playground/typings/redux-thunk/index.d.ts and then add paths overload in your tsconfig like this: "paths":{"redux-thunk":["typings/redux-thunk"]}.

const thunkAsyncAction = () => async (dispatch: Dispatch): Promise<void> => {
  // dispatch actions, return Promise, etc.
}

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

type DispatchProps = ReturnType<typeof mapDispatchToProps>;
// { thunkAsyncAction: () => Promise<void>; }

/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */
// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }

⇧ back to top


Configuration & Dev Tools

Common Npm Scripts

Common TS-related npm scripts shared across projects

"prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)",
"prettier:fix": "prettier --write 'src/**/*.ts'",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u"
"ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test",

⇧ back to top

tsconfig.json

We have recommended tsconfig.json that you can easily add to your project thanks to react-redux-typescript-scripts package.

Click to expand

{
  "compilerOptions": {
    "target": "ES6",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src",
    "typings"
  ]
}

⇧ back to top

TSLib

This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file.

https://www.npmjs.com/package/tslib

Installation
npm i tslib

Then add this to your tsconfig.json:

"compilerOptions": {
  "importHelpers": true
}

⇧ back to top

ESLint

We have recommended config that will automatically add a parser & plugin for TypeScript thanks to react-redux-typescript-scripts package.

https://typescript-eslint.io

Installation npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

.eslintrc.js

Click to expand

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  extends: ['react-app', 'react-app/jest', 'prettier'],
  rules: { 'import/no-anonymous-default-export': 0 },
};

⇧ back to top

Jest

https://jestjs.io/

Installation
npm i -D jest ts-jest @types/jest

jest.config.json

Click to expand

{
  "verbose": true,
  "transform": {
    ".(ts|tsx)": "ts-jest"
  },
  "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$",
  "moduleFileExtensions": ["ts", "tsx", "js"],
  "moduleNameMapper": {
    "^Components/(.*)": "./src/components/$1"
  },
  "globals": {
    "window": {},
    "ts-jest": {
      "tsConfig": "./tsconfig.json"
    }
  },
  "setupFiles": ["./jest.stubs.js"],
  "testURL": "http://localhost/"
}

jest.stubs.js

Click to expand

// Global/Window object Stubs for Jest
window.matchMedia = window.matchMedia || function () {
  return {
    matches: false,
    addListener: function () { },
    removeListener: function () { },
  };
};

window.requestAnimationFrame = function (callback) {
  setTimeout(callback);
};

window.localStorage = {
  getItem: function () { },
  setItem: function () { },
};

Object.values = () => [];

⇧ back to top

Style Guides

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ back to top


FAQ

Ambient Modules

Imports in ambient modules

For type augmentation imports should stay outside of module declaration.

import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' {
  interface Subject<T> {
    lift<R>(operator: Operator<T, R>): Observable<R>;
  }
}

When creating 3rd party type-definitions all the imports should be kept inside the module declaration, otherwise it will be treated as augmentation and show error

declare module "react-custom-scrollbars" {
    import * as React from "react";
    export interface positionValues {
    ...

⇧ back to top

Type-Definitions

Missing type-definitions error

if you cannot find types for a third-party module you can provide your own types or disable type-checking for this module using Shorthand Ambient Modules

// typings/modules.d.ts
declare module 'MyTypes';
declare module 'react-test-renderer';
declare module '@storybook/addon-storyshots'

Using custom d.ts files for npm modules

If you want to use an alternative (customized) type-definitions for some npm module (that usually comes with it's own type-definitions), you can do it by adding an override in paths compiler option.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one
      ...
    },
    ...,
  }
}

⇧ back to top

Type Augmentation

Strategies to fix issues coming from external type-definitions files (*.d.ts)

Augmenting library internal declarations - using relative import

// added missing autoFocus Prop on Input component in "[email protected]" npm package
declare module '../node_modules/antd/lib/input/Input' {
  export interface InputProps {
    autoFocus?: boolean;
  }
}

Augmenting library public declarations - using node_modules import

// fixed broken public type-definitions in "[email protected]" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

declare module 'rxjs/Subject' {
  interface Subject<T> {
    lift<R>(operator: Operator<T, R>): Observable<R>;
  }
}

More advanced scenarios for working with vendor type-definitions can be found here Official TypeScript Docs

⇧ back to top

Misc

- should I still use React.PropTypes in TS?

No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring Props and State interfaces, you will get complete intellisense and design-time safety with static type checking. This way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standardized method of documenting your component public API in the source code.

⇧ back to top

- when to use interface declarations and when type aliases?

From practical side, using interface declaration will create an identity (interface name) in compiler errors, on the contrary type aliases doesn't create an identity and will be unwinded to show all the properties and nested types it consists of.
Although I prefer to use type most of the time there are some places this can become too noisy when reading compiler errors and that's why I like to leverage this distinction to hide some of not so important type details in errors using interfaces identity. Related ts-lint rule: https://palantir.github.io/tslint/rules/interface-over-type-literal/

⇧ back to top

- what's better default or named exports?

A common flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
With this solution you'll achieve better encapsulation and be able to safely refactor internal naming and folders structure without breaking your consumer code:

// 1. create your component files (`select.tsx`) using default export in some folder:

// components/select.tsx
const Select: React.FC<Props> = (props) => {
...
export default Select;

// 2. in this folder create an `index.ts` file that will re-export components with named exports:

// components/index.ts
export { default as Select } from './select';
...

// 3. now you can import your components in both ways, with named export (better encapsulation) or using default export (internal access):

// containers/container.tsx
import { Select } from '@src/components';
or
import Select from '@src/components/select';
...

⇧ back to top

- how to best initialize class instance or static properties?

Prefered modern syntax is to use class Property Initializers

class ClassCounterWithInitialCount extends React.Component<Props, State> {
  // default props using Property Initializers
  static defaultProps: DefaultProps = {
    className: 'default-class',
    initialCount: 0,
  };
  
  // initial state using Property Initializers
  state: State = {
    count: this.props.initialCount,
  };
  ...
}

⇧ back to top

- how to best declare component handler functions?

Prefered modern syntax is to use Class Fields with arrow functions

class ClassCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  ...
}

⇧ back to top


Tutorials & Articles

Curated list of relevant in-depth tutorials

Higher-Order Components:

⇧ back to top


Contributors

Thanks goes to these wonderful people (emoji key):


Piotrek Witek

💻 📖 🤔 👀 💬

Kazz Yokomizo

💵 🔍

Jake Boone

📖

Amit Dahan

📖

gulderov

📖

Erik Pearson

📖

Bryan Mason

📖

Jakub Chodorowicz

💻

Oleg Maslov

🐛

Aaron Westbrook

🐛

Peter Blazejewicz

📖

Solomon White

📖

Levi Rocha

📖

Sudachi-kun

💵

Sosuke Suzuki

💻

Tom Rathbone

📖

Arshad Kazmi

📖

JeongUkJae

📖

This project follows the all-contributors specification. Contributions of any kind welcome!


MIT License

Copyright (c) 2017 Piotr Witek [email protected] (https://piotrwitek.github.io)

react-redux-typescript-guide's People

Contributors

amitdahan avatar arshadkazmi42 avatar awestbro avatar binoy14 avatar chawax avatar chillitom avatar chodorowicz avatar emp823 avatar flymason avatar gulderov avatar incerta avatar ivankoleda avatar jakeboone02 avatar jeongukjae avatar kazup01 avatar kvoncode avatar levi-rocha avatar lordi avatar louisrli avatar mathiassoeholm avatar mleg avatar peterblazejewicz avatar piotrwitek avatar rubysolo avatar schrer avatar sckelemen avatar seeergo avatar sosukesuzuki avatar stradler42 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-redux-typescript-guide's Issues

[Section] Scalable Application Structure

Add a new section about how to create a scalable structure of your application.
This section should focus on goals:

  • files grouped by features
  • easy to add/remove features
  • enable/disable feature on demand
  • features should be reusable and pluggable across different redux applications

Type assertion on the whole action

Hi,

I read about the typesafe-actions here, and I asked the question in there, but thought maybe it wouldn't hurt if I ask it here too.

I think the library it is what I have been looking for to substantially reduce boilerplate code for actions and action creators; thank you!

Creating actions (using createAction) and switching on them (using getType) in the reducer works fine, but I was wondering if there's a way to do type checking for a method that expects a specific kind of action.

Example of my actions (I'm using lodash for values and map):

export enum ActionTypes {
  GetCameras = 'GET_CAMERAS',
  GetCamerasSuccss = 'GET_CAMERAS_SUCCESS',
}

export const cameraActions = {
  getCameras: createAction(ActionType.GetCameras, ( /*...*/ ) => ({
    type: ActionTypes.GetCameras,
    /* payload: ... */
  }),
  getCamerasSuccess: createActionTypes.GetCamerasSuccess, (/*...*/) => ({
    type: ActionTypes.GetCameras,
    /* payload: ... */
  }),
};

const returnsOfActions = _.map(_.values(actions), $call));
export type CameraAction = typeof returnsOfActions[number];

Maybe a good example would be a case where the reducer wants to call another method for a specific action type, for example, in my reducer:

export const reducer = (state = initialState, action: CameraAction) => {
  case getType(cameraActions.getCameras):
    return state; /* ... */
  case getType(cameraActions.getCamerasSuccess):
    return receiveCameras(state, action);
  default:
    return state;
};

Now, in my receiveCameras method, I want to make sure the second argument is of type cameraActions.getCamerasSuccess.

I can do something like this:

const receiveCameras = (state: CameraState, action: CameraAction) => {
  if (action.type !== getType(cameraActions.getCamerasSuccess)) {
    return;
  }
  
  // Now my action type is correct
}

I was wondering if I can specify the type when defining the parameter in the method to avoid doing that if check

Thank you

suggestion: connected generic component

The repository already contains nice examples of generic components (generic-list) and connected components (sfc-counter-connected), but I'm having problems with the correct declaration and usage of connected generic components.

I would like to be able to write something like:
export const ConnectedListExtended<T> = connect<GenericListProps<T>, {}, OwnProps>(mapStateToProps)(GenericList<T>);

An example of the combination of these two examples would be really helpfull.

Thanks in advance!

Add sample with @decorator usage

The tsconfig.json sample suggests usage of experimental decorator flag. So it would be nice to show @autobind decorator removing constructor bindings.

From this:

  counter: number = 10;

  constructor(props: {}) {
    super(props);
    this.handleIncrement = this.handleIncrement.bind(this);
  }
  handleIncrement() {
    alert(`Increment ${this.counter}`);
  }
  render() {
    return (
      <SFCCounter
        count={4}
        label="Click Me"
        onIncrement={this.handleIncrement}
      />
    );
  }

to this:

import autobind from 'autobind-decorator';
...
  counter: number = 10;
  @autobind
  handleIncrement() {
    alert(`Increment ${this.counter}`);
  }
  render() {
    return (
      <SFCCounter
        count={4}
        label="Click Me"
        onIncrement={this.handleIncrement}
      />
    );
  }

Interface better than type alias.

original:

type Props = {
  className?: string,
  style?: React.CSSProperties,
};

better:

interface Props = {
  className?: string,
  style?: React.CSSProperties,
}

what do you say?

@connect decorator

It'd be great to see how react+redux+typescript can use the @connect(..) decorator syntax to connect state/actions to a component's props.

Example of chained API requests via actions?

Hello Piotr,

This guide is excellent and think this guide/repo is very useful for writing clean react/redux/typescript code.

One issue I am running into (more likely just failing to wrap my head around this) is how to handle ajax chain of requests.

For example I have a few different actions defined such as registerUser, updateProfile separated in two reducers. Now each of these actions have a corresponding epic that does an ajax call to post some data and on success returns a new action registerUserSuccess or on failure registerUserFailure. Same with updateProfile where the ajax request returns updateProfileSuccess/Failure. This all works great when called individually. What if I have a case where in another flow I have the data to both registerUser and updateProfile in the same flow and want them to occur sequentially?

Do you have any suggestions for the best way to handle this/reuse code in this scenario? Is it best to make a new action/reducer/epic flow that handles just this case, or is it possible to re-use the existing actions and have them execute sequentially depending on the result of the ajax call?

In addition it might be useful to have an API Call middleware example in the guide that allows us to reuse logic when defining <SOME_TASK>REQUEST/SUCCESS/FAIL

Suggestion: lower learning curve by including short tutorial

This is a great resource and it has helped me tremendously. Unfortunately it's hard to understand all the intricacies from the guide itself and I believe it would help a lot if there was a few step-by-step guides on for instance how to set up a small 1 or 2 component app, how to integrate each type of component and so on.

Reducer typing with redux-thunk

I've got some actions

export const setOrder = createAction('SET_ORDER', (order, id) => ({
    type: 'SET_ORDER',
    order,
    id,
}));

export const load = (id:string):ThunkAction<Promise<void>, any, void> => {
    return async (dispatch, getState):Promise<void> => {
        let result;
        try {
            result = await request(`/orders/${id}`);
            dispatch(setOrder(result, id));
        } catch (err) {
            console.error(err);
        }
    };
};

I create reducer as tutorial says:

import { getType } from 'typesafe-actions';
import { setOrder } from '../actions/order';

export type OrderState = {
    readonly loaded:string | null,
    readonly order:{}, // @TODO correct type
}

const initialState:OrderState = {
    loaded: null,
    order: {},
};

const orders = (state = initialState, action: OrdersAction) => {
    switch (action.type) {
        case getType(setOrder): {
            return {
                ...state,
                order: action.order,
                loaded: action.id,
            };
        }
        default: {
            return state;
        }
    }
};

export default orders;

import { $call } from 'utility-types';
import * as actions from '../actions/order';
const returnsOfActions = Object.values(actions).map($call);
export type OrdersAction = typeof returnsOfActions[number];

The problem is that my action type looks like:

(parameter) action: {
    type: "SET_ORDER";
    order: any;
    id: any;
} | ((dispatch: any, getState: () => any, extraArgument: void) => Promise<void>)

Looks like i should not map all actions, but select them manually. Is there any way to avoid this boilerplate?

React ChildContextProvider

I've been struggling with contextTypes in React 16.2.0 and found this solution:

import * as React from 'react';
import * as PropTypes from 'prop-types';

interface ProviderChildContext {
  blah: string;
}

export class Provider extends React.Component<void, void>
  implements React.ChildContextProvider<ProviderChildContext> {

  static childContextTypes = {
    blah: PropTypes.string
  };

  getChildContext() {
    return {
      blah: 'foo'
    };
  }
}

Do you think that something like this could be also part of this guide?

How to define OwnProps?

Can you provide an example how to define OwnProps? In below example, I was creating counter container from a purely react component. The react component takes following props.

React Component Props

export interface ICounterProps {
    label: string;
    counter: { value: number };
    isSaving: boolean;
    isLoading: boolean;
    error: string;
    increment: (n: number) => void;
    save: (n: number) => void;
    load: () => void;
}

interface State {

}

And in Container file

const mapStateToProps = (state: Store.All, OwnProps) => ({
  counter: state["@sample/counter"],
  isSaving: state["@sample/isSaving"],
  isLoading: state["@sample/isLoading"],
  error: state["@sample/error"],
})

const mapDispatchToProps = (dispatch: Function) => ({
  increment: (n: number) =>
    dispatch(incrementCounter(n)),
  load: () =>
    dispatch(loadCount(null)),
  save: (value: number) =>
    dispatch(saveCount({ value })),
})

const stateProps = returntypeof(mapStateToProps);
const dispatchProps = returntypeof(mapDispatchToProps);
type Props = typeof stateProps & typeof dispatchProps
type State = {}

export const Counter: React.ComponentClass<OwnProps> = connect(mapStateToProps, mapDispatchToProps)(CounterComponent)

In case the OwnProps should be label.

Can you provide an example for type checking JSX childrens?

microsoft/TypeScript#13618

Example: We create High-level component package with strong typed how the lower components designed to be.

type Avatar: () => JSX.Element;
interface ILoginProps {
    accounts: object;
    setFormType: Function;
    children: Avatar;
}
class Login extends React.Component<ILoginProps, ILoginState> {
    public static propTypes = {
        Avatar: React.PropTypes.func,
 }
public render(): JSX.Element {

 const { Avatar } = this.props
 return (<Avatar/>)
}

Proposal

I think we may organize the code this way. You can paste in IDE (vscode e.g.) and try typing with autocompletion and typing error highlighting.
What do you think?

type ActionCreators<TActions> = { [ P in keyof TActions ]?: (() => TActions[P])|null };
type Reducer<TState,TActions> = { [ P in keyof TActions ]?: (s: TState, a: TActions[P]) => TState };

/// --------------------------

interface RootState {
	prop1: number;
}

interface Reducer1Actions {
	expandAll: null;
	expand: { id: number };
	update: { id: number, field: string; value: any }
}

// ----------

var actions = /*createActions()*/<ActionCreators<Reducer1Actions>>({
	expand: (id: number) => ({ id }),
	update: (id: number, field: string, value: any) => ({ id, field, value })
});

var reducer = /*createReducer()*/<Reducer<RootState, Reducer1Actions>>{
	expand(s, a) { // s and a are with autocompletion
		s.prop1 = a.id; 
		return s;
	},
	update(s, a) { // s and a are with autocompletion
		s[a.field] = a.value;
		return s;
	}
}

[Section] Modelling async data with ADT

Add a new section with patterns on how to model data structures using static-typing to make the incorrect state unrepresentable. This should prevent a lot of edge cases and various display bugs by constraining our data structure to only represent correct state and also require our UI components to always handle all possible representable states when implementing a view components for the particular piece of data.

Rough sketch for this pattern:

Instead of modelling your remote data in reducer like this:

type State = {
  readonly isFetching: boolean,
  readonly error: string | null,
  readonly users: Array<User>,
};

Do it like this:

// we will use a generic union type, that can be designed specifically for our application needs
// it will set common conventions in reducers but also the way of handling different states of the components
// when fetching remote data
type RemoteData<E, D> =
  | { status: 'INITIAL' }
  | { status: 'LOADING' }
  | { status: 'ERROR', error: E }
  | { status: 'SUCCESS', data: D };

// declare your state as simple as that
type State = RemoteData<Error, Array<User>>

Now implement your component like this:

type Props = {
  usersModel: RemoteData<Error, Array<User>>,
};

const ViewComponent: React.SFC<Props> = ({ usersModel }) => {
  if (usersModel.status === 'INITIAL') {
    return null;
  } else if (usersModel.status === 'LOADING') {
    return <div>Loading...</div>;
  } else if (usersModel.status === 'ERROR') {
    return <div>An error has occurred { usersModel.error }</div>;
  }

  return (
    <>
      { usersModel.data.map(user => { ... }) }
    </>
  );
}

connect((state: State) => {
  usersModel: usersModelSelector(state),
})(ViewComponent);

actions in redux connect

The component has following props:

// MyComponent.tsx
export interface MyComponentProps {
  foo: () => any,
  bar: (n: number) => any,
}


const MyComponent: React.SFC<MyComponentProps> = (props) => {
.....
};

The container

// MyComponentContainer.ts
const mapState = (state: State) => ({
  ...
});

export default connect(mapState, {
  foo: actions.foo,
  bar: actions.bar,
})(MyComponent);

Few observations:

  1. If the action is missing then it doesn't report any error
    example: connect(mapState, {})(MyComponent); // no ts error

  2. If there is an extra action then it reports an error
    example: connect(mapState, {extra: actions.extra})(MyComponent); // ts error

  3. Wrong signature (too many arguments).
    The component expects foo: () => any, but we connect (str: string) => any. No ts error.

  4. Wrong signature (missing arguments).
    The component expects bar: (n: number) => any, but we connect () => any. A ts error will be reported.

Is it possible that we receive TS compiler errors in all cases? Especially, 1. case would be useful.

Thanks

connect stateful component

Is it expected that stateful components can't be connected to redux?
Example

// component

export interface LayoutProps {
  user?: User;
  logout: () => any;
}
type State = {
  isOpen: boolean;
};

class Layout extends React.PureComponent<LayoutProps, State> {
}

// container

const mapState = (state: State) => ({
  user: state.global.user
});
connect(mapState, {logout})(Layout);

error:

 Type 'typeof Layout' provides no match for the signature '(props: { user: User; } & { logout: (() => { type: "global/LOGOUT"; }) & TypeGetter<"global/LOGOUT">; } & { children?: ReactNode; }, context?: any): ReactElement<any>'.

It works if there are no actions

Here is my workaround

export default Layout as any as React.SFC<LayoutProps>;

Any thoughts? All libraries are the latest version.

semantic error TS4023 Exported variable 'increment' has or is using name 'TypeGetter' from external module

I am facing this error while trying to build my redux store

semantic error TS4023 Exported variable 'increment' has or is using name 'TypeGetter' from external module

Here is my source code:-

actions.ts

import { createAction } from 'typesafe-actions'

export const increment = createAction('INCREMENT')
export const add = createAction('ADD', (amount: number) => ({
    type: 'ADD',
    payload: amount,
  }))

reducer.ts

import { getType } from 'typesafe-actions';
import { RootState, RootAction } from '@src/redux/store';

import { add } from './actions';

export type CounterState = {
    readonly value: number,
};

export const initialState: CounterState = {
    value: 0,
}; 

export const CounterReducer = (state: RootState, action: RootAction) => {
    switch (action.type) {
        case getType(add):
            return state.counters.value + action.payload;
        default:
            return state.counters.value
    }
} 

store.ts

import { createStore, Store, combineReducers } from 'redux';
import * as CounterActions from '@src/redux/actions';
import { CounterReducer, CounterState } from '@src/redux/reducer';
import { $call } from 'utility-types';

export const returnsOfActions = [
  ...Object.values(CounterActions)
].map($call);

export type AppAction = typeof returnsOfActions[number];

export type RootAction = 
  | AppAction


export interface StoreEnhancerState { }

export interface RootState extends StoreEnhancerState {
  counters: CounterState;
}

const rootReducer = combineReducers<RootState>({
    counters: CounterReducer
});

function configureStore(initialState?: RootState) {
  return createStore(
    rootReducer,
    initialState!
  );
}

const store: Store<RootState> = configureStore();

export { store };

On searching a bit about the error, I found out that if I add

import { TypeGetter } from 'typesafe-actions'
export { TypeGetter }

to actions.ts, I do not get the error.

Why is it so?

Update:

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",                          
    "module": "commonjs", 
    "lib": [
      "dom",
      "es2016",
      "es2017.object"
    ],                  
    "jsx": "react",
    "declaration": false,
    "outDir": "./lib",
    "importHelpers": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmitHelpers": true,         
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "node",
    "baseUrl": "./",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "types": ["node"],
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true
  }

SFCCounterConnectedExtended does not work in examples

The initialCount property does not work on the SFCCounterConnectedExtended component.

import * as React from 'react';

import { SFCCounterConnectedExtended } from '@src/connected';

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

the initalCount is not read into the props because you are using the SFCCounter. What would be your recommendation on fixing this, create a new component or interface ?

add info about imports in module declaration

when creating 3rd party modules declarations imports should be put inside, otherwise it will be treated as augmentation and show error
Example:

declare module "react-custom-scrollbars" {
    import * as React from "react";

    export interface positionValues {
    ...

Type inference for reselect in containers

Hi,
when declaring my containers props i do something like this:

// In my selector file
const simpleSelector = (state: RootState): string => state.mydata;
const reselectSelector = createSelector([simpleSelector], (selector: string): string => selector);

// In my container file
interface SimpleContainerProps {
  simpleSelector: typeof simpleSelector; // this is ok because type is string
  reselectSelector: typeof reselectSelector; // this gives problems because type is  'OutputSelector<RootState, string, (selector: string) => string>'
}

This gives me the opportunity to be sure that if I change a selector used by any components on my app, typescript compiler will throw an error. However as you can see from my code's comments I cant use it for selectors created by reselect using createSelector.

Does anyone encountered this problem too?

suggestion: add info about dispatched action types

When declaring a connected component's property types I find it helpful to use the typeof operator to declare dispatched function types so that the property matches the action's declaration:

const actions = {
  ping: createAction('ping/PING', (arg: number) => ({
    type: 'ping/PING',
    arg,
  })),
};

interface Props {
  ping: typeof actions.ping;
}

const PingTestComponent: React.SFC<Props> = ({ping}) => {
  return (
    <Button onPress={() => ping(123)} title="ping"/>
  );
};

export const PingTest = connect(
  null,
  ({ ping: actions.ping })
)(PingTestComponent);

[Enhancement] Add a nested HOCs example with connect

Issuehunt badges

You might consider writing up techniques for dealing with prop types on components with multiple nested HOCs. For example, I frequently have components with three layers of HOCs: React-Router, Redux, and Apollo-React (graphql client).

I have a handle on how to deal, but recent changes in both Typescript and various declarations have made these stricter and harder to get right. I'm not at all confident that my design pattern for this is the best, but I'll share if you've nothing.


IssueHunt Summary

piotrwitek piotrwitek has been rewarded.

Backers (Total: $50.00)

Submitted pull Requests


Tips


IssueHunt has been backed by the following sponsors. Become a sponsor

root action

I am confused how to create the root action properly.

From docs:

// example reducer
export const reducer: Reducer<State> = (state = 0, action: RootAction) => {
// root actions
export type RootAction =
  | ReactRouterAction
  | CountersActions[keyof CountersActions]
  | TodosActions[keyof TodosActions]
  | ToastsActions[keyof ToastsActions];
// example action creators
import { createAction, getType } from 'react-redux-typescript';

// Action Creators
export const actionCreators = {
  incrementCounter: createAction('INCREMENT_COUNTER'),
  showNotification: createAction('SHOW_NOTIFICATION', 
    (message: string, severity: Severity = 'default') => ({
      type: 'SHOW_NOTIFICATION', payload: { message, severity },
    })
  ),
};

CountersActions[keyof CountersActions] is equal to action creator types, not action types.
Should I define both action creator and actions separately?
Also ToastsActions[keyof ToastsActions]; doesn't work, but typeof ToastsActions[keyof typeof ToastsActions]; works.

Misleading "with default props" section

The example shown there will not work the way we except it in JSX due to the fact that compiler uses the type of props in order to define types of attributes.

So, basically with the code given, the next code will fail:

<StatefulCounterWithDefault label="" />;
//                          ~~~~~~~~ Property 'initialCount' is missing in type '{ label: ""; }'.

There is an issue where defaultProps are being discussed

I'm not sure If that's a good idea to put the workaround I use there because it might be a bit too complicated for beginners:

export const User = class extends React.Component<PropsWithDefault, {}> {
    // ...
} as React.ComponentClass<Props>;

What do you think guys?

Error With Typescript 2.7 in react-redux-typescript/es5-commonjs/mapped-types.d.ts

(11,92): error TS2344: Type '({ [P in keyof T]: P; } & {} & { [k: string]: never; })[keyof T]' does not satisfy the constraint 'keyof (Pick<T, ({ [P in keyof T]: P; } & { [P in keyof U]: never; } & { [k: string]: never; })[ke...'.
  Type '({ [P in keyof T]: P; } & {})[keyof T]' is not assignable to type 'keyof (Pick<T, ({ [P in keyof T]: P; } & { [P in keyof U]: never; } & { [k: string]: never; })[ke...'.
    Type '{ [P in keyof T]: P; }[keyof T]' is not assignable to type 'keyof (Pick<T, ({ [P in keyof T]: P; } & { [P in keyof U]: never; } & { [k: string]: never; })[ke...'.
      Type 'keyof T' is not assignable to type 'keyof (Pick<T, ({ [P in keyof T]: P; } & { [P in keyof U]: never; } & { [k: string]: never; })[ke...'.
        Type 'keyof T' is not assignable to type 'never'.
          Type '{ [P in keyof T]: P; }[keyof T]' is not assignable to type 'never'.
            Type '({ [P in keyof T]: P; } & {})[keyof T]' is not assignable to type 'never'.
              Type '({ [P in keyof T]: P; } & {} & { [k: string]: never; })[keyof T]' is not assignable to type 'never'.
                Type '({ [P in keyof T]: P; } & {})[keyof T]' is not assignable to type 'never'.
                  Type '{ [P in keyof T]: P; }[keyof T]' is not assignable to type 'never'.
                    Type 'keyof T' is not assignable to type 'never'.

Errors on these lines:

export declare type Overwrite<T extends object, U extends object> = Pick<(Diff<T, U> & U), OmitKeys<keyof T, never>>;
export declare type Assign<T extends object, U extends object> = Pick<(Diff<T, U> & U), OmitKeys<keyof (T & U), never>>;

Better Typing for Component Callback Props

Hey there,

great guide! You recommend here to type the callbacks as () => any that did the trick for me and lead me to a better typed alternative since you know the actual type of the action creators:
typeof actionCreator.

import * as React from 'react';
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { actionCreator } from '@src/redux/actions';

interface ReduxProps {
    action: typeof actionCreator; // <- instead of: action: () => any;
}

interface Props {
    label: string;
}

interface State {
    value: string;
}

class YourComp extends React.Component<Props & ReduxProps, State> {
    public componentWillMount() {
        this.props.action();
    }

    public render() {
        // render
    }
}

function mapStateToProps(state: RootState, props: Props): Props {
    return { ...props };
}

export default connect(mapStateToProps, { action: actionCreator })(YourComp); // <- Compiler will not complain

This at least works in TypeScript 2.8 so I hope that can help to refine your guide!

Best
Vinni

Stateful component with connect

I'm using an older version of react-redux types (4.4.41) so apologies if this is incorrect. Also apologies if I've missed the example in this guide. I wanted a connected Stateful container. I appreciate that perhaps containers should be stateless as they are just there to wrap the stateful components. If this is your opinion then feel free to close this issue. Otherwise I thought it would be useful to share the typing I use:

import * as React from "react";
import {connect, Dispatch} from "react-redux";

import {AppState} from "../state/shape";
/**
* interface AppState {
*     someState: number;
* }
*/
import {saveItemAction} from "../actions/item";
/**
* export const saveItemAction = () => ({type: "SAVE_ITEM"})
*/

interface Props {
    normalProp: string;
}

interface PropsFromState {
    somePropFromState: number;
}

interface PropsFromDispatch {
    itemClicked: () => void;
}

interface AllProps extends Props, PropsFromState, PropsFromDispatch {}

class MyComponent extends React.Component<AllProps, {}> {

    render() {

        return (
        <div>
            <div>normalProp: {this.props.normalProp}</div>
            <div>somePropFromState: {this.props.somePropFromState}</div>
        </div>);
    }
}

const map_state_to_props = (state: AppState): PropsFromState => {
    return {
      somePropFromState: state.someState,
    };
};

const map_dispatch_to_props = (state: AppState, dispatch: Dispatch<AppState>): PropsFromDispatch => {
    return {
        itemClicked: () => dispatch(saveItemAction()),
    };
};

export const ConnectedMyComponent = connect(
    map_state_to_props,
    map_dispatch_to_props
)<Props>(MyComponent);

Maybe have some mistake on combineReducers?

Thank you very much for your guide, but there are some puzzles in my practice.
This guide:

import { combineReducers } from 'redux';
import { routerReducer, RouterState } from 'react-router-redux';

import { countersReducer, CountersState } from '@src/redux/counters';
import { todosReducer, TodosState } from '@src/redux/todos';

interface StoreEnhancerState { }

export interface RootState extends StoreEnhancerState {
  router: RouterState;
  counters: CountersState;
  todos: TodosState;
}

import { RootAction } from '@src/redux';
export const rootReducer = combineReducers<RootState, RootAction>({
  router: routerReducer,
  counters: countersReducer,
  todos: todosReducer,
});

combineReducers Generic with two arguments in the above example.
But in my practice ,i find combineReducers don't need two variables.
So i check the declaration file of redux.

export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;

This means that only one parameter is required.
Please confirm this question。If it is wrong, please fix it。

My redux version:3.7.2

Tried to build and got errors

$ npm run build

[email protected] prebuild c:\projects\react-redux-typescript-guide\examples
rm -rf out

[email protected] build c:\projects\react-redux-typescript-guide\examples
tsc -p ./

1 import * as uuid from 'uuid';
~~~~

src/api/articles.ts(1,13): error TS6133: 'uuid' is declared but never used.

8 const articlesResponse = require('./fixtures/articles.json');
~~~~~~~~~~~~~~~~

src/api/articles.ts(8,7): error TS6133: 'articlesResponse' is declared but never used.

7 import { actionCreators } from '../modules/converter';
~~~~~~~~~~~~~~~~~~~~~~

src/components/connected-stateless-counter.tsx(7,32): error TS2307: Cannot find module '../modules/converter'.

14 counter: state.converter.counter,
~~~~~~~

src/components/connected-stateless-counter.tsx(14,28): error TS2339: Property 'counter' does not exist on type 'IState'.

24 const checkProps: IStatelessCounterProps = { ...stateProps, ...dispatchProps };
~~~~~~~~~~

src/components/connected-stateless-counter.tsx(24,7): error TS6133: 'checkProps' is declared but never used.

8
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/containers/home.tsx(8,32): error TS2322: Type '{ label: "ConnectedStatelessCounter"; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<IStatelessCounterProps, ComponentState>>...'.
Type '{ label: "ConnectedStatelessCounter"; }' is not assignable to type 'Readonly'.
Property 'counter' is missing in type '{ label: "ConnectedStatelessCounter"; }'.

example of state merging in reducer not compliant with TS v2.4

Hi,

Thank you for the guide.

How would you recommend the example on "Spread operation with Exact Types check to guard against excess or mismatched props" be used with strictNullChecks? As a Partial makes all properties nullable, it is required to define all State in this form:

export type State = {
  readonly counter: number | undefined,
  readonly baseCurrency: string | undefined,
};

This is the same as effectively switching strictNullChecks off. Is there any other way?

Cheers.

Hall of Contributors

The Guide is growing strong and I would like to add a Hall of Contributors.
I'm looking for some simple solution that will generate contributors list from JSON, the result would be nice markdown/html table with heads and icons describing the kind of contribution.
If someone know such solution please share some links here. Thanks.

Incorrect action creator type when using bindActionCreators and redux-thunk

Specifically I have this action defined.

getTransitionForm: (params: GetTransitionFormParams) =>
    (dispatch: Dispatch<RootState>, getState: () => RootState, extraArgument: any) => 
         Promise<any>;

When mapped as follows as per your guide from section "Connected Container without OwnProps using Type Inference" https://github.com/piotrwitek/react-redux-typescript-guide#connected-container-with-ownprops

export const mapDispatchToProps = (dispatch: Dispatch<RootState>) =>
  bindActionCreators(
    {
      getTransitionForm,
    },
    dispatch
  );
const dispatchProps = returntypeof(mapDispatchToProps);

The resulting value of dispatchProps is

const dispatchProps: {
    getTransitionForm: (params: GetTransitionFormParams) =>
        (dispatch: Dispatch<RootState>, getState: () => RootState, extraArgument: any) => 
             Promise<any>;
}

I believe after binding it should be.

const dispatchProps: {
    getTransitionForm: (params: GetTransitionFormParams) => Promise<any>;
}

Which I expect is not going to be easy to derive.
I only discovered this as I wanted to do something with the returned promise.
Thanks for your great guide.

Syntax error with $call and createAction

I am getting this TS compile error

./src/store/ducks/auth.module.ts
Syntax error: Unexpected token, expected ] (95:41)
//My actions
export const actions = {
  signup: createAction(types.SIGNUP_REQUEST, (email: string, password: string) => ({
    type: types.SIGNUP_REQUEST,
    payload: { email, password },
  })),
....

  93 | 
  94 | const returnsOfActions = _.values(actions).map($call);
> 95 | type AppAction = typeof returnsOfActions[number];

It seems to be mad about the [number] part. Any idea on what the issue is?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.