Skip to main content

Работа с TypeScript

Начиная с 8-ой версии, React-Redux полностью написан на TypeScript, тем самым типизация включена в распространяемый пакет. Вдобавок, типизация содержит в себе несколько помощников (helpers) для упрощения написания типобезопасных интерфейсов между Redux хранилищем(store) и React компонентами.

Информация

В недавно обновлённой основной версии @types/react@18 изменилось определение компонентов, был удален пропс children, существовавший по умолчанию. В случае наличия несколько копий @types/react в вашем проекте, это будет вызывать ошибки. Для исправления воспользуйтесь вашим менеджером пакетов, чтобы привести @types/react к единственной версии. Детали:

https://github.com/facebook/react/issues/24304#issuecomment-1094565891

Стандартная настройка проекта с Redux Toolkit и TypeScript

Мы предполагаем, что типичный Redux проект использует Redux Toolkit вместе с React Redux.

Redux Toolkit (RTK) — стандартный подход для написания современной Redux логики. RTK написан на TypeScript и его API разработано с целью дать хороший опыт вместе с TypeScript.

Шаблон Redux+TS для Create-React-App содержит настроенный проект.

Определение корневого состояние и типов для отправки (dispatch)

Использование configureStore не нуждается в дополнительной типизации. Несмотря на это вам следуют извлечь типы RootState и Dispatch, чтобы на них можно было ссылаться по мере необходимости. Извлечение этих типов из хранилища(store) означает, что они будут корректно обновляться по мере добавления новых частей хранилища(store) или при модификации настроек middleware.

Так как они являются типами, можно не переживать о безопасности при экспорте их из файла с настройкой хранилища app/store.ts и последующем импортировании в других файлах.

app/store.ts
import { configureStore } from '@reduxjs/toolkit'
// ...

const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer,
},
})

// Выведение типов `RootState` и `AppDispatch` из хранилища
export type RootState = ReturnType<typeof store.getState>
// Выведенные типы: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch

Определяем типизированные хуки

Пускай, имеется возможность импортировать типы RootState и AppDispatch в каждый компонент, лучше создать типизированные версии хуков useDispatch и useSelector. Это важно по нескольким причинам:

  • useSelector избавляет вас от необходимости каждый раз печатать (state: RootState)
  • useDispatch: тип Dispatch по умолчанию не знаком с thunks. С целью корректной отправки thunks, вам необходимо использовать специальный тип AppDispatch из хранилища (store), который включает типы из thunk middleware и использует их вместе с useDispatch. С помощью типизированного хука useDispatch можно забыть о необходимости импортировать AppDispatch.

Поскольку это переменные, а не типы, важно определить их в отдельном файле, таком как app/hooks.ts, а не в файле настройки хранилища(store). Это позволит вам импортировать их в любой компонент, который должен использовать хуки, и избежать потенциальную проблему с импортом из-за циклической зависимости.

app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// Используйте во всем приложении вместо обычных `useDispatch` и `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

Ручная типизация хуков

Мы рекомендуем использовать хуки useAppSelector и useAppDispatch, показанные выше. Если вы предпочитаете их не использовать, то здесь мы покажем, как типизировать сами хуки.

Типизация хука useSelector

Когда вы пишете функцию селектор в useSelector, вам следует явно определить тип параметра state. Так Typescript сможет вывести тип возвращаемого значения функции селектора, который в свою очередь будет использован как возвращаемый тип самого хука useSelector:

interface RootState {
isOn: boolean
}

// TS выводит тип: (state: RootState) => boolean
const selectIsOn = (state: RootState) => state.isOn

// TS выводит `isOn` как boolean
const isOn = useSelector(selectIsOn)

This can also be done inline as well:

const isOn = useSelector((state: RootState) => state.isOn)

Типизация хука useDispatch

По умолчанию возвращаемое значение useDispatch - стандартный тип Dispatch, определённый в основных типах Redux, следовательно типизация не потребуется:

const dispatch = useDispatch()

Если у вы изменяете тип Dispatch, вы можете использовать его явно:

// store.ts
export type AppDispatch = typeof store.dispatch

// MyComponent.tsx
const dispatch: AppDispatch = useDispatch()

Типизация компонента высшего порядка connect

Автовыведение типов подключенных пропсов

connect содержит 2 функции, вызывающиеся одна за другой. Первая функция в качестве аргументов принимает mapState и mapDispatch и возвращает вторую функцию. Вторая функция принимает компонент для того, чтобы обёрнуть его, и возвращает новый компонент обёртку, который принимает пропсы из mapState и mapDispatch. Обычно обе функции вызываются вместе: connect(mapState, mapDispatch)(MyComponent).

В пакет включен помощник для типов, ConnectedProps, который может извлечь типы возвращаемых значений mapStateToProps и mapDispatchToProps из первой функции. Это означает, что при разъединении вызова connect на 2 шага, все "пропсы из Redux" могут быть выведены автоматически, без необходимости писать их вручную. Этот подход может показаться непривычным, если вы использовали React-Redux ранее, это значительно упрощает объявления типов.

import { connect, ConnectedProps } from 'react-redux'

interface RootState {
isOn: boolean
}

const mapState = (state: RootState) => ({
isOn: state.isOn,
})

const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' }),
}

const connector = connect(mapState, mapDispatch)

// Выведенный тип будет выглядеть так:
// {isOn: boolean, toggleOn: () => void}
type PropsFromRedux = ConnectedProps<typeof connector>

Возвращаемый тип из ConnectedProps может быть использован для типизации ваших пропсов.

interface Props extends PropsFromRedux {
backgroundColor: string
}

const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)

export default connector(MyComponent)

В силу того, что типы могут быть определены в любом порядке, при желании вы всё ещё можете объявить ваш компонент перед объявлением функции connector.

// Альтернативно объявляем `type Props = PropsFromRedux & {backgroundColor: string}`
interface Props extends PropsFromRedux {
backgroundColor: string;
}

const MyComponent = (props: Props) => /* также как выше */

const connector = connect(/* также как выше */)

type PropsFromRedux = ConnectedProps<typeof connector>

export default connector(MyComponent)

Ручная типизация connect

Компонент высшего порядка connect несколько сложно типизировать, ведь у него 3 источника пропсов: mapStateToProps, mapDispatchToProps и пропсы, переданные из родительского компонента. Здесь полный пример, как выглядит его типизация вручную:

import { connect } from 'react-redux'

interface StateProps {
isOn: boolean
}

interface DispatchProps {
toggleOn: () => void
}

interface OwnProps {
backgroundColor: string
}

type Props = StateProps & DispatchProps & OwnProps

const mapState = (state: RootState) => ({
isOn: state.isOn,
})

const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' }),
}

const MyComponent = (props: Props) => (
<div style={{ backgroundColor: props.backgroundColor }}>
<button onClick={props.toggleOn}>
Toggle is {props.isOn ? 'ON' : 'OFF'}
</button>
</div>
)

// Типичное использование: функция `connect` вызывается после определения компонента
export default connect<StateProps, DispatchProps, OwnProps>(
mapState,
mapDispatch
)(MyComponent)

Также возможно несколько сократить это, выведением типов mapState и mapDispatch:

const mapState = (state: RootState) => ({
isOn: state.isOn,
})

const mapDispatch = {
toggleOn: () => ({ type: 'TOGGLE_IS_ON' }),
}

type StateProps = ReturnType<typeof mapState>
type DispatchProps = typeof mapDispatch

type Props = StateProps & DispatchProps & OwnProps

Тем не менее, выведение типа mapDispatch так не будет работать, если он определен как объект, а также ссылается на thunk'и.

Рекомендации

API хуков в целом проще использовать со статическими типами. Если вы ищете наиболее простое решение для использования статических типов с React-Redux, посмотриет API хуков.

Если вы используете connect, мы рекомендуем использовать ConnectedProps<T> подход для выведения типов пропсов из Redux, так как это требует наименьшего количества явных объявлений типа.

Ресурсы

Для дополнительной информации посмотрите: