From 9fd3344f0ff628c0c606ac9112ae4f3b5fe7398e Mon Sep 17 00:00:00 2001 From: JNaftali Date: Tue, 29 Oct 2019 21:58:45 -0400 Subject: [PATCH 1/6] first draft of new page on static types --- docs/using-react-redux/static-types.md | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/using-react-redux/static-types.md diff --git a/docs/using-react-redux/static-types.md b/docs/using-react-redux/static-types.md new file mode 100644 index 000000000..b8147327d --- /dev/null +++ b/docs/using-react-redux/static-types.md @@ -0,0 +1,106 @@ +--- +id: static-types +title: Type safety and React-Redux +hide_title: true +sidebar_label: Type safety and React-Redux +--- + +# Type safety and React-Redux + +The [`react-redux` type definitions](https://npm.im/@types/react-redux) export some helpers to make it easier to write typesafe interfaces between your redux store and your React components. + +## Typing the useSelector hook + +If you manually type your selector functions, `useSelector` will return the same type as your selector + +```ts +// selectIsOn always returns a boolean, so isOn is typed as a boolean +const isOn = useSelector(selectIsOn) +``` + +Otherwise `useSelector` requires you to manually type the state argument every time you use it. + +```ts +const isOn = useSelector((state: MyStateType) => state.isOn) +``` + +If you'd like, you can define a typed `useSelect` hook using a helper type + +```ts +// reducer.ts +import { useSelector, TypedUseSelectorHook } from 'react-redux' + +interface MyStateType { + isOn: boolean +} + +export const useTypedSelector: TypedUseSelectorHook = useSelector + +// my-component.tsx +import { useTypedSelector } from './reducer.ts' + +const isOn = useSelector(state => state.isOn) +``` + +## Typing the `connect` higher order component + +React-redux exposes a helper type, `ConnectedProps`, that can extract the return type of mapStateToProps and mapDispatchToProps. + +```ts +import { connect, ConnectedProps } from 'react-redux' + +interface MyStateType { + isOn: boolean +} + +const connector = connect( + (state: MyStateType) => ({ + isOn: state.isOn + }), + (dispatch: any) => ({ + toggleOn: () => dispatch({ type: 'TOGGLE_IS_ON' }) + }) +) + +type PropsFromRedux = ConnectedProps +``` + +The return type of `ConnectedProps` can then be used to type your props object. + +```tsx +interface Props extends PropsFromRedux { + backgroundColor: string +} + +export const MyComponent = connector((props: Props) => ( +
+ +
+)) +``` + +If you are using untyped selectors or for some other reason would rather, you can also pass type arguments to `connect` and use those same types to create your own props type. + +```ts +interface StateProps { + isOn: boolean +} + +interface DispatchProps { + toggleOn: () => void +} + +interface OwnProps { + backgroundColor: string +} + +type Props = StateProps & DispatchProps & OwnProps + +const connector = connect< + StateProps, + DispatchProps, + OwnProps +>(/* arguments as above */) +``` From e56b27d6907eb04fc75a184a578b2fa432acc96f Mon Sep 17 00:00:00 2001 From: JNaftali Date: Sat, 9 Nov 2019 17:01:27 -0500 Subject: [PATCH 2/6] revised because feedback --- docs/using-react-redux/static-types.md | 88 ++++++++++++++++++-------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/docs/using-react-redux/static-types.md b/docs/using-react-redux/static-types.md index b8147327d..e87dd4e9d 100644 --- a/docs/using-react-redux/static-types.md +++ b/docs/using-react-redux/static-types.md @@ -1,5 +1,5 @@ --- -id: static-types +id: static-typing title: Type safety and React-Redux hide_title: true sidebar_label: Type safety and React-Redux @@ -7,24 +7,25 @@ sidebar_label: Type safety and React-Redux # Type safety and React-Redux -The [`react-redux` type definitions](https://npm.im/@types/react-redux) export some helpers to make it easier to write typesafe interfaces between your redux store and your React components. +React-redux doesn't ship with types. If you are using Typescript you should install the [`react-redux` type definitions](https://npm.im/@types/react-redux) from npm. In addition to typing the library functions, the types also export some helpers to make it easier to write typesafe interfaces between your redux store and your React components. ## Typing the useSelector hook -If you manually type your selector functions, `useSelector` will return the same type as your selector +If your selector functions has declared a return type, `useSelector` will return that same type ```ts -// selectIsOn always returns a boolean, so isOn is typed as a boolean +const selectIsOn = (state: MyStateType) => state.isOn + const isOn = useSelector(selectIsOn) ``` -Otherwise `useSelector` requires you to manually type the state argument every time you use it. +Passing an inline function to `useSelector` requires you to manually type the state argument. ```ts const isOn = useSelector((state: MyStateType) => state.isOn) ``` -If you'd like, you can define a typed `useSelect` hook using a helper type +If you want to avoid repeating the `state` type declaration, you can define a typed `useSelect` hook using a helper type exported by `@types/react-redux` ```ts // reducer.ts @@ -44,7 +45,44 @@ const isOn = useSelector(state => state.isOn) ## Typing the `connect` higher order component -React-redux exposes a helper type, `ConnectedProps`, that can extract the return type of mapStateToProps and mapDispatchToProps. +Traditionally typing components connected to the redux store using `connect` has been a bit laborious. Here's a full example. + +```ts +import { connect } from 'react-redux' + +interface StateProps { + isOn: boolean +} + +interface DispatchProps { + toggleOn: () => void +} + +interface OwnProps { + backgroundColor: string +} + +type Props = StateProps & DispatchProps & OwnProps + +const MyComponent = (props: Props) => ( +
+ +
+) + +export default connect( + (state: MyStateType) => ({ + isOn: state.isOn + }), + { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) + } +)(MyComponent) +``` + +React-redux exposes a helper type, `ConnectedProps`, that can extract the return types of `mapStateToProp` and `mapDispatchToProps` from a `connector` function. ```ts import { connect, ConnectedProps } from 'react-redux' @@ -57,9 +95,9 @@ const connector = connect( (state: MyStateType) => ({ isOn: state.isOn }), - (dispatch: any) => ({ - toggleOn: () => dispatch({ type: 'TOGGLE_IS_ON' }) - }) + { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) + } ) type PropsFromRedux = ConnectedProps @@ -72,35 +110,29 @@ interface Props extends PropsFromRedux { backgroundColor: string } -export const MyComponent = connector((props: Props) => ( +const MyComponent = (props: Props) => (
-)) +) + +export default connector(MyComponent) ``` -If you are using untyped selectors or for some other reason would rather, you can also pass type arguments to `connect` and use those same types to create your own props type. +Because types can be defined in any order, you can still declare your component before declaring the connector if you want. -```ts -interface StateProps { - isOn: boolean +```tsx +interface Props extends PropsFromRedux { + backgroundColor: string; } -interface DispatchProps { - toggleOn: () => void -} +const MyComponent = (props: Props) => /* same as above */ -interface OwnProps { - backgroundColor: string -} +const connector = connect(/* same as above*/) -type Props = StateProps & DispatchProps & OwnProps +type PropsFromRedux = ConnectedProps -const connector = connect< - StateProps, - DispatchProps, - OwnProps ->(/* arguments as above */) +export default connector(MyComponent) ``` From 32a24f47acc7020b44e7a2c46992ae107deb7226 Mon Sep 17 00:00:00 2001 From: JNaftali Date: Sat, 9 Nov 2019 18:31:24 -0500 Subject: [PATCH 3/6] mention that we're splitting up the export and connect call --- docs/using-react-redux/static-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using-react-redux/static-types.md b/docs/using-react-redux/static-types.md index e87dd4e9d..eef75f6c3 100644 --- a/docs/using-react-redux/static-types.md +++ b/docs/using-react-redux/static-types.md @@ -45,7 +45,7 @@ const isOn = useSelector(state => state.isOn) ## Typing the `connect` higher order component -Traditionally typing components connected to the redux store using `connect` has been a bit laborious. Here's a full example. +The `connect` higher-order function can be a bit laborious to type, because there are 3 sources of props: mapStateToProps, mapDispatchToProps, and props passed in from the parent component. Here's a full example of what it looks like to do that manually. ```ts import { connect } from 'react-redux' @@ -82,7 +82,7 @@ export default connect( )(MyComponent) ``` -React-redux exposes a helper type, `ConnectedProps`, that can extract the return types of `mapStateToProp` and `mapDispatchToProps` from a `connector` function. +React-redux exposes a helper type, `ConnectedProps`, that can extract the return types of `mapStateToProp` and `mapDispatchToProps` from a `connector` function, although this means that creating the connector and exporting the connected component needs to be done in 2 separate steps. ```ts import { connect, ConnectedProps } from 'react-redux' From 53f7a5592f6bbbd9751cd51ace8d39994186d274 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 23 Nov 2019 13:44:00 -0500 Subject: [PATCH 4/6] Update "Static Types" content --- docs/using-react-redux/static-types.md | 141 +++++++++++++++++++------ 1 file changed, 111 insertions(+), 30 deletions(-) diff --git a/docs/using-react-redux/static-types.md b/docs/using-react-redux/static-types.md index eef75f6c3..384516bdd 100644 --- a/docs/using-react-redux/static-types.md +++ b/docs/using-react-redux/static-types.md @@ -1,41 +1,61 @@ --- id: static-typing -title: Type safety and React-Redux +title: Static Typing hide_title: true -sidebar_label: Type safety and React-Redux +sidebar_label: Static Typing --- -# Type safety and React-Redux +# Static Typing -React-redux doesn't ship with types. If you are using Typescript you should install the [`react-redux` type definitions](https://npm.im/@types/react-redux) from npm. In addition to typing the library functions, the types also export some helpers to make it easier to write typesafe interfaces between your redux store and your React components. +React-Redux is currently written in plain JavaScript. However, it works well with static type systems such as TypeScript and Flow. -## Typing the useSelector hook +## TypeScript -If your selector functions has declared a return type, `useSelector` will return that same type +React-Redux doesn't ship with its own type definitions. If you are using Typescript you should install the [`@types/react-redux` type definitions](https://npm.im/@types/react-redux) from npm. In addition to typing the library functions, the types also export some helpers to make it easier to write typesafe interfaces between your Redux store and your React components. -```ts -const selectIsOn = (state: MyStateType) => state.isOn +### Defining the Root State Type -const isOn = useSelector(selectIsOn) +Both `mapState` and `useSelector` depend on declaring the type of the complete Redux store state value. While this type could be written by hand, the easiest way to define it is to have TypeScript infer it based on what your root reducer function returns. This way, the type is automatically updated as the reducer functions are modified. + +```ts +// rootReducer.ts +export const rootReducer = combineReducers({ + posts: postsReducer, + comments: commentsReducer, + users: usersReducer +}) + +export type RootState = ReturnType +// {posts: PostsState, comments: CommentsState, users: UsersState} ``` -Passing an inline function to `useSelector` requires you to manually type the state argument. +### Typing the useSelector hook + +When writing selector functions for use with `useSelector`, you should explicitly define the type of the `state` parameter. TS should be able to then infer the return type of the selector, which will be reused as the return type of the `useSelector` hook: ```ts -const isOn = useSelector((state: MyStateType) => state.isOn) +interface RootState { + isOn: boolean +} + +// TS infers type: (state: RootState) => boolean +const selectIsOn = (state: RootState) => state.isOn + +// TS infers `isOn` is boolean +const isOn = useSelector(selectIsOn) ``` -If you want to avoid repeating the `state` type declaration, you can define a typed `useSelect` hook using a helper type exported by `@types/react-redux` +If you want to avoid repeating the `state` type declaration, you can define a typed `useSelect` hook using a helper type exported by `@types/react-redux`: ```ts // reducer.ts import { useSelector, TypedUseSelectorHook } from 'react-redux' -interface MyStateType { +interface RootState { isOn: boolean } -export const useTypedSelector: TypedUseSelectorHook = useSelector +export const useTypedSelector: TypedUseSelectorHook = useSelector // my-component.tsx import { useTypedSelector } from './reducer.ts' @@ -43,9 +63,29 @@ import { useTypedSelector } from './reducer.ts' const isOn = useSelector(state => state.isOn) ``` -## Typing the `connect` higher order component +### Typing the `useDispatch` hook + +By default, the return value of `useDispatch` is the standard `Dispatch` type defined by the Redux core types, so no declarations are needed: + +```ts +const dispatch = useDispatch() +``` + +If you have a customized version of the `Dispatch` type, you may use that type explicitly: + +```ts +// store.ts +export type AppDispatch = typeof store.dispatch + +// MyComponent.tsx +const dispatch: AppDispatch = useDispatch() +``` + +### Typing the `connect` higher order component + +#### Manually Typing `connect` -The `connect` higher-order function can be a bit laborious to type, because there are 3 sources of props: mapStateToProps, mapDispatchToProps, and props passed in from the parent component. Here's a full example of what it looks like to do that manually. +The `connect` higher-order component is somewhat complex to type, because there are 3 sources of props: `mapStateToProps`, `mapDispatchToProps`, and props passed in from the parent component. Here's a full example of what it looks like to do that manually. ```ts import { connect } from 'react-redux' @@ -64,6 +104,14 @@ interface OwnProps { type Props = StateProps & DispatchProps & OwnProps +const mapState = (state: RootState) => ({ + isOn: state.isOn +}) + +const mapDispatch = { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) +} + const MyComponent = (props: Props) => (
) +// Typical usage: `connect` is called after the component is defined export default connect( - (state: MyStateType) => ({ - isOn: state.isOn - }), - { - toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) - } + mapState, + mapDispatch )(MyComponent) ``` -React-redux exposes a helper type, `ConnectedProps`, that can extract the return types of `mapStateToProp` and `mapDispatchToProps` from a `connector` function, although this means that creating the connector and exporting the connected component needs to be done in 2 separate steps. +It is also possible to shorten this somewhat, by inferring the types of `mapState` and `mapDispatch`: + +```ts +const mapState = (state: RootState) => ({ + isOn: state.isOn +}) + +const mapDispatch = { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) +} + +type StateProps = ReturnType +type DispatchProps = typeof mapDispatch + +type Props = StateProps & DispatchProps & OwnProps +``` + +However, inferring the type of `mapDispatch` this way will break if it is defined as an object and also refers to thunks. + +#### Inferring The Connected Props Automatically + +`connect` consists of two functions that are called sequentially. The first function accepts `mapState` and `mapDispatch` as arguments, and returns a second function. The second function accepts the component to be wrapped, and returns a new wrapper component that passes down the props from `mapState` and `mapDispatch`. Normally, both functions are called together, like `connect(mapState, mapDispatch)(MyComponent)`. + +As of v7.1.2, the `@types/react-redux` package exposes a helper type, `ConnectedProps`, that can extract the return types of `mapStateToProp` and `mapDispatchToProps` from the first function. This means that if you split the `connect` call into two steps, all of the "props from Redux" can be inferred automatically without having to write them by hand. While this approach may feel unusual if you've been using React-Redux for a while, it does simplify the type declarations considerably. ```ts import { connect, ConnectedProps } from 'react-redux' -interface MyStateType { +interface RootState { isOn: boolean } +const mapState = (state: RootState) => ({ + isOn: state.isOn +}) + +const mapDispatch = { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) +} + const connector = connect( - (state: MyStateType) => ({ - isOn: state.isOn - }), - { - toggleOn: () => ({ type: 'TOGGLE_IS_ON' }) - } + mapState, + mapDispatch ) +// The inferred type will look like: +// {isOn: boolean, toggleOn: () => void} type PropsFromRedux = ConnectedProps ``` @@ -124,6 +198,7 @@ export default connector(MyComponent) Because types can be defined in any order, you can still declare your component before declaring the connector if you want. ```tsx +// alternately, declare `type Props = Props From Redux & {backgroundColor: string}` interface Props extends PropsFromRedux { backgroundColor: string; } @@ -136,3 +211,9 @@ type PropsFromRedux = ConnectedProps export default connector(MyComponent) ``` + +### Recommendations + +The hooks API is generally simpler to use with static types. **If you're looking for the easiest solution for using static types with React-Redux, use the hooks API.** + +If you're using `connect`, **we recommend using the `ConnectedProps` approach for inferring the props from Redux**, as that requires the fewest explicit type declarations. From 2bc483a95159729962f268e9d949634c19dc5d7a Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 23 Nov 2019 13:44:10 -0500 Subject: [PATCH 5/6] Add "Static Typing" page to sidebar --- website/sidebars.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/sidebars.json b/website/sidebars.json index d2947ec6d..549068a4a 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -8,7 +8,8 @@ "Using React Redux": [ "using-react-redux/connect-mapstate", "using-react-redux/connect-mapdispatch", - "using-react-redux/accessing-store" + "using-react-redux/accessing-store", + "using-react-redux/static-typing" ], "API Reference": [ "api/connect", From 314233692bf5c4f0e2f0407fcf1bd1eb72410ea2 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sat, 23 Nov 2019 14:59:58 -0500 Subject: [PATCH 6/6] Move Static Typing page to get it to show up in 7.1 sidebar --- .../version-7.1}/using-react-redux/static-types.md | 5 +++-- website/versioned_sidebars/version-7.1-sidebars.json | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename {docs => website/versioned_docs/version-7.1}/using-react-redux/static-types.md (99%) diff --git a/docs/using-react-redux/static-types.md b/website/versioned_docs/version-7.1/using-react-redux/static-types.md similarity index 99% rename from docs/using-react-redux/static-types.md rename to website/versioned_docs/version-7.1/using-react-redux/static-types.md index 384516bdd..4109ea484 100644 --- a/docs/using-react-redux/static-types.md +++ b/website/versioned_docs/version-7.1/using-react-redux/static-types.md @@ -1,5 +1,6 @@ --- -id: static-typing +id: version-7.1-static-typing +original_id: static-typing title: Static Typing hide_title: true sidebar_label: Static Typing @@ -87,7 +88,7 @@ const dispatch: AppDispatch = useDispatch() The `connect` higher-order component is somewhat complex to type, because there are 3 sources of props: `mapStateToProps`, `mapDispatchToProps`, and props passed in from the parent component. Here's a full example of what it looks like to do that manually. -```ts +```tsx import { connect } from 'react-redux' interface StateProps { diff --git a/website/versioned_sidebars/version-7.1-sidebars.json b/website/versioned_sidebars/version-7.1-sidebars.json index e184f7756..e40f6bf4d 100644 --- a/website/versioned_sidebars/version-7.1-sidebars.json +++ b/website/versioned_sidebars/version-7.1-sidebars.json @@ -8,7 +8,8 @@ "Using React Redux": [ "version-7.1-using-react-redux/connect-mapstate", "version-7.1-using-react-redux/connect-mapdispatch", - "version-7.1-using-react-redux/accessing-store" + "version-7.1-using-react-redux/accessing-store", + "version-7.1-using-react-redux/static-typing" ], "API Reference": [ "version-7.1-api/connect", @@ -17,8 +18,6 @@ "version-7.1-api/batch", "version-7.1-api/hooks" ], - "Guides": [ - "version-7.1-troubleshooting" - ] + "Guides": ["version-7.1-troubleshooting"] } }