Home > Enterprise >  Reusable way to avoid declaring an Interface for every action in Redux?
Reusable way to avoid declaring an Interface for every action in Redux?

Time:04-25

I recently jumped into TypeScript to code my projects with React/Redux (-Saga) and something has bothered me.

As you can see in the code below, declaring an interface for each action looks fine as there isn't much inside the file but this might not look so good over time with more actions (e.g. edit/delete), especially for the type declaration.

actions/articles.ts

import { IArticle, IStoreAction } from '../../../interfaces';

import { createAction } from './createAction';


export enum ActionTypes {
    createArticleRequested = 'articles/createArticleRequested',
    createArticleSucceeded = 'articles/createArticleSucceeded',
    createArticleFailed = 'articles/createArticleFailed',
}


interface ICreateArticleRequestAction {
    type: ActionTypes.createArticleRequested,
    article: IArticle;
}

interface ICreateArticleFailureAction {
    type: ActionTypes.createArticleFailed,
    error: string;
}

interface ICreateArticleSuccessAction {
    type: ActionTypes.createArticleSucceeded,
    article: IArticle;
}


export type ArticlesAction = 
    ICreateArticleRequestAction | 
    ICreateArticleSuccessAction | 
    ICreateArticleFailureAction;


export const createArticleRequest = (article: IArticle)
    : IStoreAction<typeof article> => createAction(
        ActionTypes.createArticleRequested,
        article
    );

export const createArticleSuccess = (article: IArticle)
    : IStoreAction<typeof article> => createAction(
        ActionTypes.createArticleSucceeded,
        article
    );

export const createArticleFailure = (error: string)
    : IStoreAction<typeof error> => createAction(
        ActionTypes.createArticleFailed,
        error
    );

I tried to develop a reusable solution that could be based on a single interface but it usaully came with its set of errors. Is there a better existing setup to make the whole thing cleaner and more reusable?

Thank you for your attention!

CodePudding user response:

Yes, use the official Redux Toolkit, which is the official recommendation for any Redux code written nowadays (since 2019).

See Why Redux Toolkit is How To Use Redux Today.

It makes all those action type interfaces completely obsolete (we even consider it an antipattern to build those action union types) and also does away with switch..case reducers, ACTION_TYPES, hand-written action creators, immutable reducer logic, createStore/applyMiddleware. Also, modern Redux does not use connect/mapStateToProps.

It cuts your code down to about 25% of hand-written legacy Redux code.

I'd recommend going through the official Redux Essentials Tutorial which starts Redux Toolkit from the beginning.

CodePudding user response:

@phry is my Redux Toolkit co-maintainer, and to tack on to what he said, the right way to write a Redux reducer file with TS looks like this (per our TypeScript Quick Start page):

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// Define a type for the slice state
interface CounterState {
  value: number
}

// Define the initial state using that type
const initialState: CounterState = {
  value: 0
}

export const counterSlice = createSlice({
  name: 'counter',
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    increment: state => {
      state.value  = 1
    },
    decrement: state => {
      state.value -= 1
    },
    // Use the PayloadAction type to declare the contents of `action.payload`
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value  = action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

Note that all you need is a type for the slice reducer's state, and action: PayloadAction<SomePayloadTypeHere> for each case reducer.

In fact, we specifically advise against trying to write TS union types for actions.

  • Related