diff --git a/README.md b/README.md index 647b5c71..56e8a6fd 100644 --- a/README.md +++ b/README.md @@ -1,2268 +1,271 @@ -
- # 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](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/react-redux-ts) -[![Join the chat at https://gitter.im/react-redux-typescript-guide/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/react-redux-typescript-guide/Lobby) - -_Found it useful? Want more updates?_ - -[**Show your support by giving a :star:**](https://github.com/piotrwitek/react-redux-typescript-guide/stargazers) - - - Buy Me a Coffee - - - Become a Patron - - -

- -## **What's new?** - -:tada: _Now updated to support **TypeScript v4.6**_ :tada: -:rocket: _Updated to `typesafe-actions@5.x` :rocket: - -

- -
- -### **Goals** - -- Complete type safety (with [`--strict`](https://www.typescriptlang.org/docs/handbook/compiler-options.html) 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) - -### **React, Redux, Typescript Ecosystem** - -- [typesafe-actions](https://github.com/piotrwitek/typesafe-actions) - Typesafe utilities for "action-creators" in Redux / Flux Architecture -- [utility-types](https://github.com/piotrwitek/utility-types) - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types. -- [react-redux-typescript-scripts](https://github.com/piotrwitek/react-redux-typescript-scripts) - dev-tools configuration files shared between projects based on this guide - -### **Examples** - -- Todo-App playground: [Codesandbox](https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox) -- React, Redux, TypeScript - RealWorld App: [Github](https://github.com/piotrwitek/react-redux-typescript-realworld-app) | [Demo](https://react-redux-typescript-realworld-app.netlify.com/) +> **"This guide will teach you how to use React (and its Ecosystem) in a functional style with TypeScript."** -### **Playground Project** +## What is this guide? -[![Build Status](https://semaphoreci.com/api/v1/piotrekwitek/react-redux-typescript-guide/branches/master/shields_badge.svg)](https://semaphoreci.com/piotrekwitek/react-redux-typescript-guide) +This is a **practical reference guide** that shows you how to use TypeScript with React and Redux through real-world code examples. It's not a boilerplate or starter templateβ€”it's a collection of patterns, type definitions, and best practices you can copy into your own projects. -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. +### Who is this for? -## Contributing Guide +- **React developers** learning TypeScript +- **TypeScript developers** learning React +- **Teams** wanting consistent patterns for React + TypeScript projects +- **Beginners** who want to understand how to properly type React components, Redux stores, and async actions -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](/CONTRIBUTING.md) +### What you'll learn -## Funding +- How to type React components (functional and class-based) +- How to type Redux stores, actions, and reducers +- How to type React hooks (`useState`, `useEffect`, `useContext`, etc.) +- How to type async operations and side effects +- How to type HOCs (Higher-Order Components) and render props +- How to type forms, events, and refs +- Common TypeScript patterns that make React code safer and more maintainable -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. +### How to use this guide -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. +1. **Browse by topic** - Use the table of contents to jump to the pattern you need +2. **Copy examples** - Each section has copy-paste-ready code snippets +3. **Understand the types** - We explain what each TypeScript type does and why it's useful +4. **Try the playground** - Run `npm install && npm start` in the `playground` folder to see live examples -[![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/76996763) +> πŸ’‘ **Note for beginners**: Types like `React.FC`, `React.ComponentProps`, and `Dispatch` come from the `@types/react` and `@types/react-redux` packagesβ€”they're not custom to this guide. We show you how to use the official types that TypeScript provides for React. --- -🌟 - _New or updated section_ - ## Table of Contents - - - [React Types Cheatsheet](#react-types-cheatsheet) - - [`React.FC` | `React.FunctionComponent`](#reactfcprops--reactfunctioncomponentprops) - - [`React.Component`](#reactcomponentprops-state) - - [`React.ComponentType`](#reactcomponenttypeprops) - - [`React.ComponentProps`](#reactcomponentpropstypeof-xxx) - - [`React.ReactElement` | `JSX.Element`](#reactreactelement--jsxelement) - - [`React.ReactNode`](#reactreactnode) - - [`React.CSSProperties`](#reactcssproperties) - - [`React.XXXHTMLAttributes`](#reactxxxhtmlattributeshtmlxxxelement) - - [`React.ReactEventHandler`](#reactreacteventhandlerhtmlxxxelement) - - [`React.XXXEvent`](#reactxxxeventhtmlxxxelement) -- [React](#react) - - [Function Components - FC](#function-components---fc) - - [- Counter Component](#--counter-component) - - [- Counter Component with default props](#--counter-component-with-default-props) - - [- Spreading attributes in Component](#--spreading-attributes-in-component) - - [Class Components](#class-components) - - [- Class Counter Component](#--class-counter-component) - - [- Class Component with default props](#--class-component-with-default-props) - - [Generic Components](#generic-components) - - [- Generic List Component](#--generic-list-component) - - [Hooks](#hooks) - - [- useState](#--usestate) - - [- useContext](#--usecontext) - - [- useReducer](#--usereducer) - - [Render Props](#render-props) - - [- Name Provider Component](#--name-provider-component) - - [- Mouse Provider Component](#--mouse-provider-component) - - [Higher-Order Components](#higher-order-components) - - [- HOC wrapping a component](#--hoc-wrapping-a-component) - - [- HOC wrapping a component and injecting props](#--hoc-wrapping-a-component-and-injecting-props) - - [- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟](#--nested-hoc---wrapping-a-component-injecting-props-and-connecting-to-redux-) - - [Redux Connected Components](#redux-connected-components) - - [- Redux connected counter](#--redux-connected-counter) - - [- Redux connected counter with own props](#--redux-connected-counter-with-own-props) - - [- Redux connected counter via hooks](#--redux-connected-counter-via-hooks) - - [- Redux connected counter with `redux-thunk` integration](#--redux-connected-counter-with-redux-thunk-integration) - - [Context](#context) - - [ThemeContext](#themecontext) - - [ThemeProvider](#themeprovider) - - [ThemeConsumer](#themeconsumer) - - [ThemeConsumer in class component](#themeconsumer-in-class-component) -- [Redux](#redux) - - [Store Configuration](#store-configuration) - - [Create Global Store Types](#create-global-store-types) - - [Create Store](#create-store) - - [Action Creators 🌟](#action-creators-) - - [Reducers](#reducers) - - [State with Type-level Immutability](#state-with-type-level-immutability) - - [Typing reducer](#typing-reducer) - - [Typing reducer with `typesafe-actions`](#typing-reducer-with-typesafe-actions) - - [Testing reducer](#testing-reducer) - - [Async Flow with `redux-observable`](#async-flow-with-redux-observable) - - [Typing epics](#typing-epics) - - [Testing epics](#testing-epics) - - [Selectors with `reselect`](#selectors-with-reselect) - - [Connect with `react-redux`](#connect-with-react-redux) - - [Typing connected component](#typing-connected-component) - - [Typing `useSelector` and `useDispatch`](#typing-useselector-and-usedispatch) - - [Typing connected component with `redux-thunk` integration](#typing-connected-component-with-redux-thunk-integration) -- [Configuration & Dev Tools](#configuration--dev-tools) - - [Common Npm Scripts](#common-npm-scripts) - - [tsconfig.json](#tsconfigjson) - - [TSLib](#tslib) - - [ESLint](#eslint) - - [.eslintrc.js](#eslintrcjs) - - [Jest](#jest) - - [jest.config.json](#jestconfigjson) - - [jest.stubs.js](#jeststubsjs) - - [Style Guides](#style-guides) - - [react-styleguidist](#react-styleguidist) -- [FAQ](#faq) - - [Ambient Modules](#ambient-modules) - - [Imports in ambient modules](#imports-in-ambient-modules) - - [Type-Definitions](#type-definitions) - - [Missing type-definitions error](#missing-type-definitions-error) - - [Using custom `d.ts` files for npm modules](#using-custom-dts-files-for-npm-modules) - - [Type Augmentation](#type-augmentation) - - [Augmenting library internal declarations - using relative import](#augmenting-library-internal-declarations---using-relative-import) - - [Augmenting library public declarations - using node_modules import](#augmenting-library-public-declarations---using-node_modules-import) - - [Misc](#misc) - - [- should I still use React.PropTypes in TS?](#--should-i-still-use-reactproptypes-in-ts) - - [- when to use `interface` declarations and when `type` aliases?](#--when-to-use-interface-declarations-and-when-type-aliases) - - [- what's better default or named exports?](#--whats-better-default-or-named-exports) - - [- how to best initialize class instance or static properties?](#--how-to-best-initialize-class-instance-or-static-properties) - - [- how to best declare component handler functions?](#--how-to-best-declare-component-handler-functions) -- [Tutorials & Articles](#tutorials--articles) -- [Contributors](#contributors) - --- -# 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` +## Goals -> *NB: Guide is based on types for Redux >= v4.x.x. - -[⇧ back to top](#table-of-contents) +- **Complete type safety** - Catch errors at compile time, not runtime +- **Functional programming style** - Leverage TypeScript's type inference with modern React patterns (hooks, functional components) +- **Practical examples** - Real-world patterns you can use immediately +- **Standard types** - Use official types from `@types/react` and `@types/react-redux` (no custom magic) +- **Minimal boilerplate** - Write less code while getting more type safety --- -## React Types Cheatsheet - -### `React.FC` | `React.FunctionComponent` - -Type representing a functional component - -```tsx -const MyComponent: React.FC = ... -``` - -### `React.Component` - -Type representing a class component - -```tsx -class MyComponent extends React.Component { ... -``` - -### `React.ComponentType` - -Type representing union of (`React.FC | React.Component`) - used in HOC - -```tsx -const withState =

( - WrappedComponent: React.ComponentType

, -) => { ... -``` - -### `React.ComponentProps` - -Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props) - -```tsx -type MyComponentProps = React.ComponentProps; -``` - -### `React.ReactElement` | `JSX.Element` - -Type representing a concept of React Element - representation of a native DOM component (e.g. `

`), or a user-defined composite component (e.g. ``) - -```tsx -const elementOnly: React.ReactElement =
|| ; -``` - -### `React.ReactNode` - -Type representing any possible type of React node (basically ReactElement (including Fragments and Portals) + primitive JS types) - -```tsx -const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined ||
|| ; -const Component = ({ children: React.ReactNode }) => ... -``` - -### `React.CSSProperties` - -Type representing style object in JSX - for css-in-js styles - -```tsx -const styles: React.CSSProperties = { flexDirection: 'row', ... -const element =
` - -Type representing HTML attributes of specified HTML Element - for extending HTML Elements - -```tsx -const Input: React.FC> = props => { ... } - - -``` - -### `React.ReactEventHandler` - -Type representing generic event handler - for declaring event handlers - -```tsx -const handleChange: React.ReactEventHandler = (ev) => { ... } - - -``` - -### `React.XXXEvent` - -Type representing more specific event. Some common event examples: `ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent`. - -```tsx -const handleChange = (ev: React.MouseEvent) => { ... } - -
-``` +## Complementary Projects -In code above `React.MouseEvent` is type of mouse event, and this event happened on `HTMLDivElement` +This guide focuses on **type patterns**. For complete project setups, see: -[⇧ back to top](#table-of-contents) +- [react-redux-typescript-jslib-starter](https://github.com/piotrwitek/react-redux-typescript-jslib-starter) - Ready-to-use starter with build config, testing, and CI +- [typesafe-actions](https://github.com/piotrwitek/typesafe-actions) - Type-safe Redux action creators and reducers +- [utility-types](https://github.com/piotrwitek/utility-types) - Reusable TypeScript utility types --- -# React - -## Function Components - FC +## Understanding the Playground -### - Counter Component +The `playground` folder contains a working React app that demonstrates all the patterns in this guide. -```tsx -import * as React from 'react'; - -type Props = { - label: string; - count: number; - onIncrement: () => void; -}; - -export const FCCounter: React.FC = props => { - const { label, count, onIncrement } = props; - - const handleIncrement = () => { - onIncrement(); - }; - - return ( -
- - {label}: {count} - - -
- ); -}; +**To run it:** +```bash +cd playground +npm install +npm start ``` -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fccounter) +The playground uses [react-styleguidist](https://react-styleguidist.js.org/) to generate interactive documentation. It automatically finds all components in the `src/components` folder and displays them with their props, usage examples, and live previews. -[⇧ back to top](#table-of-contents) +**How it works:** -### - Counter Component with default props +- Styleguidist scans `src/components/**/*.tsx` for components +- It reads JSDoc comments and TypeScript types to generate documentation +- `*.md` files next to components become usage examples +- The rendered page shows ALL components, not just what's in `App.tsx` -```tsx -import * as React from 'react'; +So if you see more components in the browser than in the code, that's whyβ€”styleguidist is showing you the entire component library, not just a single app entry point. -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 ( -
- - {label}: {count} - - -
- ); -}; +## React Types Cheatsheet -FCCounterWithDefaultProps.defaultProps = { count: 5 }; +Before diving into the guide, here's a quick reference of the most common React types you'll see: -``` +### From `@types/react`: -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fccounterwithdefaultprops) +- **`React.FC`** - Type for functional components (includes `children` by default in older versions) +- **`React.ComponentProps`** - Extract props type from a component +- **`React.ReactNode`** - Anything React can render (elements, strings, numbers, arrays, fragments, etc.) +- **`React.ReactElement`** - A React element created with JSX or `createElement` +- **`React.CSSProperties`** - Type for inline styles +- **`React.HTMLAttributes`** - HTML attributes for a specific element +- **`React.ChangeEvent`** - Type for input change events +- **`React.FormEvent`** - Type for form submit events +- **`React.MouseEvent`** - Type for click events +- **`React.useRef(null)`** - Type for refs to DOM elements +- **`React.useState('')`** - Type for state (usually inferred automatically) -[⇧ back to top](#table-of-contents) +### From `@types/react-redux`: -### - [Spreading attributes](https://facebook.github.io/react/docs/jsx-in-depth.html#spread-attributes) in Component +- **`useSelector`** - Type-safe Redux state selector +- **`useDispatch()`** - Type-safe Redux dispatch +- **`connect()`** - Type-safe Redux connect HOC -```tsx -import * as React from 'react'; +These are **standard types** provided by the TypeScript definitions for React and Redux. This guide shows you how to use them effectively. -type Props = React.PropsWithChildren<{ - className?: string; - style?: React.CSSProperties; -}>; +--- -export const FCSpreadAttributes: React.FC = (props) => { - const { children, ...restProps } = props; +## Installation - return
{children}
; -}; +Types for React and Redux are installed separately from the libraries themselves: +```bash +npm install --save react react-dom redux react-redux +npm install --save-dev @types/react @types/react-dom @types/react-redux ``` -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#fcspreadattributes) - -[⇧ back to top](#table-of-contents) +For hooks support, use React 16.8+ and @types/react 16.8+. --- -## Class Components - -### - Class Counter Component - -```tsx -import * as React from 'react'; - -type Props = { - label: string; -}; - -type State = { - count: number; -}; - -export class ClassCounter extends React.Component { - 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 ( -
- - {label}: {count} - - -
- ); - } -} - -``` - -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#classcounter) +## React - Type Definitions Cheatsheet -[⇧ back to top](#table-of-contents) +### Function Components -### - Class Component with default props +**Basic functional component:** ```tsx -import * as React from 'react'; +import React from 'react'; type Props = { - label: string; - initialCount: number; + name: string; + age: number; }; -type State = { - count: number; +// Modern approach (React 18+) +const MyComponent = ({ name, age }: Props) => { + return
{name} is {age} years old
; }; -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 ( -
- - {label}: {count} - - -
- ); - } -} - -``` - -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#classcounterwithdefaultprops) - -[⇧ back to top](#table-of-contents) - ---- - -## Generic Components - -- easily create typed component variations and reuse common logic -- common use case is a generic list components - -### - Generic List Component - -```tsx -import * as React from 'react'; - -export interface GenericListProps { - items: T[]; - itemRenderer: (item: T) => JSX.Element; -} - -export class GenericList extends React.Component, {}> { - render() { - const { items, itemRenderer } = this.props; - - return ( -
- {items.map(itemRenderer)} -
- ); - } -} - +// With React.FC (includes children automatically in older versions) +const MyComponent: React.FC = ({ name, age }) => { + return
{name} is {age} years old
; +}; ``` -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#genericlist) - -[⇧ back to top](#table-of-contents) +**Why this matters:** TypeScript will now catch typos in prop names, wrong prop types, and missing required props at compile time instead of runtime. --- -## Hooks - -> +### Hooks -### - useState - -> +**useState with explicit type:** ```tsx -import * as React from 'react'; - -type Props = { initialCount: number }; - -export default function Counter({initialCount}: Props) { - const [count, setCount] = React.useState(initialCount); - return ( - <> - Count: {count} - - - - - ); -} - +const [count, setCount] = React.useState(0); +const [user, setUser] = React.useState(null); ``` -[⇧ back to top](#table-of-contents) - -### - useContext - -> +**useRef for DOM elements:** ```tsx -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 ( - - ); -} +const inputRef = React.useRef(null); +// Later: +inputRef.current?.focus(); ``` -[⇧ back to top](#table-of-contents) - -### - useReducer - -> +**useReducer with typed actions:** ```tsx -import * as React from 'react'; +type State = { count: number }; +type Action = { type: 'increment' } | { type: 'decrement' }; -interface State { - count: number; -} - -type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' }; - -function reducer(state: State, action: Action): State { +const 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} - - - - - ); -} - -export default Counter; - -``` - -[⇧ back to top](#table-of-contents) - ---- - -## Render Props - -> - -### - Name Provider Component - -Simple component using children as a render prop - -```tsx -import * as React from 'react'; - -interface NameProviderProps { - children: (state: NameProviderState) => React.ReactNode; -} - -interface NameProviderState { - readonly name: string; -} - -export class NameProvider extends React.Component { - readonly state: NameProviderState = { name: 'Piotr' }; - - render() { - return this.props.children(this.state); - } -} - -``` - -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#nameprovider) - -[⇧ back to top](#table-of-contents) - -### - Mouse Provider Component - -`Mouse` component found in [Render Props React Docs](https://reactjs.org/docs/render-props.html#use-render-props-for-cross-cutting-concerns) - -```tsx -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 { - readonly state: MouseProviderState = { x: 0, y: 0 }; - - handleMouseMove = (event: React.MouseEvent) => { - this.setState({ - x: event.clientX, - y: event.clientY, - }); - }; - - render() { - return ( -
- {/* - Instead of providing a static representation of what renders, - use the `render` prop to dynamically determine what to render. - */} - {this.props.render(this.state)} -
- ); + case 'increment': return { count: state.count + 1 }; + case 'decrement': return { count: state.count - 1 }; } -} - -``` - -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/#mouseprovider) - -[⇧ back to top](#table-of-contents) - ---- - -## Higher-Order Components - -> - -### - HOC wrapping a component - -Adds state to a stateless counter - -```tsx -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 = ( - BaseComponent: React.ComponentType -) => { - type HocProps = Diff & { - // here you can extend hoc with new props - initialCount?: number; - }; - type HocState = { - readonly count: number; - }; - - return class Hoc extends React.Component { - // 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 ( - - ); - } - }; }; +const [state, dispatch] = React.useReducer(reducer, { count: 0 }); ``` -
Click to expand

- -```tsx -import * as React from 'react'; - -import { withState } from '../hoc'; -import { FCCounter } from '../components'; - -const FCCounterWithState = withState(FCCounter); - -export default () => ; - -``` -

- -[⇧ back to top](#table-of-contents) -### - HOC wrapping a component and injecting props - -Adds error handling using componentDidCatch to any component - -```tsx -import React from 'react'; +--- -const MISSING_ERROR = 'Error was swallowed during propagation.'; - -export const withErrorBoundary = ( - BaseComponent: React.ComponentType -) => { - 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 { - // 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 ; - } - - return children; - } - }; -}; +### Events -``` -
Click to expand

+**Typing event handlers:** ```tsx -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 handleChange = (e: React.ChangeEvent) => { + console.log(e.target.value); }; -const BrokenButton = () => { - const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] = - useState(false); - - if (shouldRenderBrokenComponent) { - return ; - } - - return ( - - ); +const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); }; -export default () => ( - - - -); - -``` -

- -[⇧ back to top](#table-of-contents) - -### - Nested HOC - wrapping a component, injecting props and connecting to redux 🌟 - -Adds error handling using componentDidCatch to any component - -```tsx -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 = ( - BaseComponent: React.ComponentType -) => { - const mapStateToProps = (state: RootState) => ({ - count: countersSelectors.getReduxCounter(state.counters), - }); - - const dispatchProps = { - onIncrement: countersActions.increment, - }; - - type HocProps = ReturnType & - typeof dispatchProps & { - // here you can extend ConnectedHoc with new props - overrideCount?: number; - }; - - class Hoc extends React.Component { - // 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 ( - - ); - } - } - - const ConnectedHoc = connect< - ReturnType, - typeof dispatchProps, // use "undefined" if NOT using dispatchProps - Diff, - RootState - >( - mapStateToProps, - dispatchProps - )(Hoc); - - return ConnectedHoc; +const handleClick = (e: React.MouseEvent) => { + console.log('clicked'); }; - -``` -
Click to expand

- -```tsx -import * as React from 'react'; - -import { withConnectedCount } from '../hoc'; -import { FCCounter } from '../components'; - -const FCCounterWithConnectedCount = withConnectedCount(FCCounter); - -export default () => ( - -); - ``` -

- -[⇧ back to top](#table-of-contents) --- -## Redux Connected Components +## Redux - Type Definitions Cheatsheet -### - Redux connected counter +**Typed Redux store:** ```tsx -import Types from 'MyTypes'; -import { connect } from 'react-redux'; +import { createStore } from 'redux'; +import { useSelector, useDispatch } 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, +// 1. Define your state shape +type RootState = { + counter: number; + user: User | null; }; -export const FCCounterConnected = connect( - mapStateToProps, - dispatchProps -)(FCCounter); - -``` -
Click to expand

- -```tsx -import * as React from 'react'; - -import { FCCounterConnected } from '.'; - -export default () => ; - -``` -

- -[⇧ back to top](#table-of-contents) - -### - Redux connected counter with own props +// 2. Define your actions +type Action = + | { type: 'INCREMENT' } + | { type: 'DECREMENT' } + | { type: 'SET_USER'; payload: User }; -```tsx -import Types from 'MyTypes'; -import { connect } from 'react-redux'; - -import { countersActions, countersSelectors } from '../features/counters'; -import { FCCounter } from '../components'; - -type OwnProps = { - initialCount?: number; +// 3. Create typed reducer +const rootReducer = (state: RootState, action: Action): RootState => { + // ... reducer logic }; -const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({ - count: - countersSelectors.getReduxCounter(state.counters) + - (ownProps.initialCount || 0), -}); +// 4. Create store +const store = createStore(rootReducer); +type AppDispatch = typeof store.dispatch; -const dispatchProps = { - onIncrement: countersActions.increment, +// 5. Use in components +const MyComponent = () => { + const count = useSelector((state: RootState) => state.counter); + const dispatch = useDispatch(); + + return ; }; - -export const FCCounterConnectedOwnProps = connect( - mapStateToProps, - dispatchProps -)(FCCounter); - -``` -
Click to expand

- -```tsx -import * as React from 'react'; - -import { FCCounterConnectedOwnProps } from '.'; - -export default () => ( - -); - ``` -

- -[⇧ back to top](#table-of-contents) - -### - Redux connected counter via hooks - -```tsx -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 dispatch(increment())}/>; -}; -export default FCCounterConnectedHooksUsage; +--- -``` +## Contributing -[⇧ back to top](#table-of-contents) +Contributions are welcome! Please read the contributing guidelines before submitting PRs. -### - Redux connected counter with `redux-thunk` integration +--- -```tsx -import Types from 'MyTypes'; -import { bindActionCreators, Dispatch } from 'redux'; -import { connect } from 'react-redux'; -import * as React from 'react'; +## License -import { countersActions } from '../features/counters'; +MIT License -// Thunk Action -const incrementWithDelay = () => async (dispatch: Dispatch): Promise => { - setTimeout(() => dispatch(countersActions.increment()), 1000); -}; +--- -const mapStateToProps = (state: Types.RootState) => ({ - count: state.counters.reduxCounter, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( - { - onIncrement: incrementWithDelay, - }, - dispatch - ); - -type Props = ReturnType & - ReturnType & { - label: string; - }; - -export const FCCounter: React.FC = props => { - const { label, count, onIncrement } = props; - - const handleIncrement = () => { - // Thunk action is correctly typed as promise - onIncrement().then(() => { - // ... - }); - }; - - return ( -
- - {label}: {count} - - -
- ); -}; - -export const FCCounterConnectedBindActionCreators = connect( - mapStateToProps, - mapDispatchToProps -)(FCCounter); - -``` -
Click to expand

- -```tsx -import * as React from 'react'; - -import { FCCounterConnectedBindActionCreators } from '.'; - -export default () => ( - -); - -``` -

- -[⇧ back to top](#table-of-contents) - -## Context - -> - -### ThemeContext - -```tsx -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({ theme: themes.light }); - -export default ThemeContext; - -``` - -[⇧ back to top](#table-of-contents) - -### ThemeProvider - -```tsx -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 ( - - - - ); - } -} - -``` - -[⇧ back to top](#table-of-contents) - -### ThemeConsumer - -```tsx -import * as React from 'react'; -import ThemeContext from './theme-context'; - -type Props = {}; - -export default function ToggleThemeButton(props: Props) { - return ( - - {({ theme, toggleTheme }) => - ); - } -} - -``` - -[Implementation with Hooks](#--usecontext) - -[⇧ back to top](#table-of-contents) - - ---- - -# 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 - -```tsx -import { StateType, ActionType } from 'typesafe-actions'; - -declare module 'MyTypes' { - export type Store = StateType; - export type RootAction = ActionType; - export type RootState = StateType>; -} - -declare module 'typesafe-actions' { - interface Types { - RootAction: ActionType; - } -} - -``` - -[⇧ back to top](#table-of-contents) - -### 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 - -```tsx -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`](https://github.com/piotrwitek/typesafe-actions#typesafe-actions) [![Latest Stable Version](https://img.shields.io/npm/v/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions) [![NPM Downloads](https://img.shields.io/npm/dt/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions) 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](https://github.com/piotrwitek/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. - -```tsx -/* 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)(); -export const payloadAction = createAction(ADD)(); -export const payloadMetaAction = createAction(ADD)(); - -export const payloadCreatorAction = createAction( - 'TOGGLE_TODO', - (todo: Todo) => todo.id -)(); - -``` -
Click to expand

- -```tsx -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](#table-of-contents) - ---- - -## Reducers - -### State with Type-level Immutability - -Declare reducer `State` type with `readonly` modifier to get compile time immutability - -```ts -export type State = { - readonly counter: number; - readonly todos: ReadonlyArray; -}; -``` - -Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors - -```ts -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`,...). - -```ts -state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray' -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](https://www.typescriptlang.org/docs/handbook/advanced-types.html) - -```ts -export type State = Readonly<{ - counterPairs: ReadonlyArray>, -}>; - -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`](https://github.com/piotrwitek/utility-types#deepreadonlyt) type (available from `utility-types`). - -```ts -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](#table-of-contents) - -### Typing reducer - -> to understand following section make sure to learn about [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html), [Control flow analysis](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#control-flow-based-type-analysis) and [Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types) - -```tsx -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; - -export type TodosState = Readonly<{ - todos: Todo[]; - todosFilter: TodosFilter; -}>; -const initialState: TodosState = { - todos: [], - todosFilter: TodosFilter.All, -}; - -export default combineReducers({ - 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](#table-of-contents) - -### 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. - -```tsx -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](#table-of-contents) - -### Testing reducer - -```tsx -import { - todosReducer as reducer, - todosActions as actions, -} from './'; -import { TodosState } from './reducer'; - -/** - * FIXTURES - */ -const getInitialState = (initial?: Partial) => - 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](#table-of-contents) - ---- - -## Async Flow with `redux-observable` - -### Typing epics - -```tsx -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 = ( - 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](#table-of-contents) - -### Testing epics - -```tsx -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; - - beforeEach(() => { - state$ = new StateObservable( - new Subject(), - 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](#table-of-contents) - ---- - -## Selectors with `reselect` - -```tsx -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](#table-of-contents) - ---- - -## 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](#redux-connected-components) section._ - -```tsx -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) => - 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) => - bindActionCreators>({ - invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... } - }, dispatch); - -``` - -[⇧ back to top](#table-of-contents) - -### Typing `useSelector` and `useDispatch` - -```tsx -import { Dispatch } from 'redux'; -import { - TypedUseSelectorHook, - useSelector as useGenericSelector, - useDispatch as useGenericDispatch -} from 'react-redux'; -import { RootState, RootAction } from 'MyTypes'; - -export const useSelector: TypedUseSelectorHook = useGenericSelector; - -export const useDispatch: () => Dispatch = useGenericDispatch; - -``` - -[⇧ back to top](#table-of-contents) - -### 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`](./playground/typings/redux-thunk/index.d.ts) and then add `paths` overload in your tsconfig like this: [`"paths":{"redux-thunk":["typings/redux-thunk"]}`](./playground/tsconfig.json)._ - -```tsx -const thunkAsyncAction = () => async (dispatch: Dispatch): Promise => { - // dispatch actions, return Promise, etc. -} - -const mapDispatchToProps = (dispatch: Dispatch) => - bindActionCreators( - { - thunkAsyncAction, - }, - dispatch - ); - -type DispatchProps = ReturnType; -// { thunkAsyncAction: () => Promise; } - -/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */ -// { thunkAsyncAction: () => (dispatch: Dispatch) => Promise; } -``` - -[⇧ back to top](#table-of-contents) - ---- - -# Configuration & Dev Tools - -## Common Npm Scripts - -> Common TS-related npm scripts shared across projects - -```json -"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](#table-of-contents) - -## tsconfig.json - -We have recommended `tsconfig.json` that you can easily add to your project thanks to [`react-redux-typescript-scripts`](https://github.com/piotrwitek/react-redux-typescript-scripts) package. - -
Click to expand

- -```tsx -{ - "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](#table-of-contents) - -## TSLib - -This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file. - -> - -> Installation -`npm i tslib` - - -Then add this to your `tsconfig.json`: - -```ts -"compilerOptions": { - "importHelpers": true -} -``` - -[⇧ back to top](#table-of-contents) - -## ESLint - -We have recommended config that will automatically add a parser & plugin for TypeScript thanks to [`react-redux-typescript-scripts`](https://github.com/piotrwitek/react-redux-typescript-scripts) package. - -> - -> Installation -`npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin` - - -### .eslintrc.js - -
Click to expand

- -```tsx -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](#table-of-contents) - -## Jest - -> - -> Installation -`npm i -D jest ts-jest @types/jest` - -### jest.config.json - -
Click to expand

- -```tsx -{ - "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

- -```tsx -// 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](#table-of-contents) - -## Style Guides - -### [react-styleguidist](https://github.com/styleguidist/react-styleguidist) - -[⟩⟩⟩ styleguide.config.js](/playground/styleguide.config.js) - -[⟩⟩⟩ demo](https://piotrwitek.github.io/react-redux-typescript-guide/) - -[⇧ back to top](#table-of-contents) - ---- - -# FAQ - - -## Ambient Modules - -### Imports in ambient modules - -For type augmentation imports should stay outside of module declaration. - -```ts -import { Operator } from 'rxjs/Operator'; -import { Observable } from 'rxjs/Observable'; - -declare module 'rxjs/Subject' { - interface Subject { - lift(operator: Operator): Observable; - } -} -``` - -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 - -```ts -declare module "react-custom-scrollbars" { - import * as React from "react"; - export interface positionValues { - ... -``` - -[⇧ back to top](#table-of-contents) - -## 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](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Modules.md#shorthand-ambient-modules) - -```tsx -// 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. - -```ts -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "redux": ["typings/redux"], // use an alternative type-definitions instead of the included one - ... - }, - ..., - } -} -``` - -[⇧ back to top](#table-of-contents) - -## Type Augmentation - -Strategies to fix issues coming from external type-definitions files (*.d.ts) - -### Augmenting library internal declarations - using relative import - -```ts -// added missing autoFocus Prop on Input component in "antd@2.10.0" npm package -declare module '../node_modules/antd/lib/input/Input' { - export interface InputProps { - autoFocus?: boolean; - } -} -``` - -### Augmenting library public declarations - using node_modules import - -```ts -// fixed broken public type-definitions in "rxjs@5.4.1" npm package -import { Operator } from 'rxjs/Operator'; -import { Observable } from 'rxjs/Observable'; - -declare module 'rxjs/Subject' { - interface Subject { - lift(operator: Operator): Observable; - } -} -``` - -> More advanced scenarios for working with vendor type-definitions can be found here [Official TypeScript Docs](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Modules.md#working-with-other-javascript-libraries) - -[⇧ back to top](#table-of-contents) - -## 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](#table-of-contents) - -### - 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: - -[⇧ back to top](#table-of-contents) - -### - 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: - -```ts -// 1. create your component files (`select.tsx`) using default export in some folder: - -// components/select.tsx -const Select: React.FC = (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](#table-of-contents) - -### - how to best initialize class instance or static properties? - -Prefered modern syntax is to use class Property Initializers - -```tsx -class ClassCounterWithInitialCount extends React.Component { - // 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](#table-of-contents) - -### - how to best declare component handler functions? - -Prefered modern syntax is to use Class Fields with arrow functions - -```tsx -class ClassCounter extends React.Component { -// handlers using Class Fields with arrow functions - handleIncrement = () => { - this.setState({ count: this.state.count + 1 }); - }; - ... -} -``` - -[⇧ back to top](#table-of-contents) - ---- - -# Tutorials & Articles - -> Curated list of relevant in-depth tutorials - -Higher-Order Components: - -- - -[⇧ back to top](#table-of-contents) - ---- - -# Contributors - -Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): - - - -| [
Piotrek Witek](https://github.com/piotrwitek)
[πŸ’»](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Code") [πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=piotrwitek "Documentation") [πŸ€”](#ideas-piotrwitek "Ideas, Planning, & Feedback") [πŸ‘€](#review-piotrwitek "Reviewed Pull Requests") [πŸ’¬](#question-piotrwitek "Answering Questions") | [
Kazz Yokomizo](https://github.com/kazup01)
[πŸ’΅](#financial-kazup01 "Financial") [πŸ”](#fundingFinding-kazup01 "Funding Finding") | [
Jake Boone](https://github.com/jakeboone02)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=jakeboone02 "Documentation") | [
Amit Dahan](https://github.com/amitdahan)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=amitdahan "Documentation") | [
gulderov](https://github.com/gulderov)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=gulderov "Documentation") | [
Erik Pearson](https://github.com/emp823)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=emp823 "Documentation") | [
Bryan Mason](https://github.com/flymason)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=flymason "Documentation") | -| :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| [
Jakub Chodorowicz](http://www.jakub.chodorowicz.pl/)
[πŸ’»](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chodorowicz "Code") | [
Oleg Maslov](https://github.com/mleg)
[πŸ›](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Amleg "Bug reports") | [
Aaron Westbrook](https://github.com/awestbro)
[πŸ›](https://github.com/piotrwitek/react-redux-typescript-guide/issues?q=author%3Aawestbro "Bug reports") | [
Peter Blazejewicz](http://www.linkedin.com/in/peterblazejewicz)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=peterblazejewicz "Documentation") | [
Solomon White](https://github.com/rubysolo)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=rubysolo "Documentation") | [
Levi Rocha](https://github.com/pino)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=pino "Documentation") | [
Sudachi-kun](http://cloudnative.co.jp)
[πŸ’΅](#financial-loadbalance-sudachi-kun "Financial") | -| [
Sosuke Suzuki](http://sosukesuzuki.github.io)
[πŸ’»](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=sosukesuzuki "Code") | [
Tom Rathbone](https://github.com/chillitom)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=chillitom "Documentation") | [
Arshad Kazmi](https://arshadkazmi42.github.io/)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=arshadkazmi42 "Documentation") | [
JeongUkJae](https://jeongukjae.github.io)
[πŸ“–](https://github.com/piotrwitek/react-redux-typescript-guide/commits?author=JeongUkJae "Documentation") | - - -This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! - ---- - -MIT License +## Acknowledgements -Copyright (c) 2017 Piotr Witek () +Maintained by [Piotr Witek](https://github.com/piotrwitek). Special thanks to all contributors who have helped improve this guide.