DEV Community

Hector Osuna
Hector Osuna

Posted on • Originally published at fangoh.dev

Vanilla Use Reducer with TypeScript

Hi, I was recently doing a project for my portfolio, a React Project with a slightly complex state. Personally, I really love TypeScript and the Redux/Reducer patterns, because they give me a great Developer Experience with TypeScript and a lot of autocompletion available, quite useful in order to not get bit by a bug caused by a typo, and really convenient as I don't have to explore files constantly in order to know what are the properties that a certain object should have.

I always prefer to use TS over vanilla JS, so, it makes a lot of sense to try to make use of strict typing, even in a concept app. Also, while vanilla useReducer might not be what most projects use, I still want to share how I found it to be pretty useful in this context.

The problem

My application is a Social Media-like platform, so a very important part is the use of a user, whose information lives in the global state, that I'm going to access constantly for making requests to the server and other stuff. In order to keep my Client-Side code tidy and predictable, I built a state into a UserReducer which is going to store the user info and make changes, this is then exposed in an almost global context in order to be accessible anywhere in the frontend.

So, my goals in typing the Reducer, are quite simple:

  • Get better intellisense whenever I need to dispatch an action.
  • Make quite simple the construction of an Action Creator.
  • Having appropriate intellisense in the action.payload inside the reducer.

The solution

Typing the state

More often than not, the initial state isn't going to be a great example of how the usual state will look like, so it's pretty important to declare a State Type, in my particular use case, I'm going to propose the following typing:

interface UserState {
    isLogged: boolean;
    userID: string | null;
    username: string | null;
}
Enter fullscreen mode Exit fullscreen mode

I must type userID and username as string or null, as null it's my default state whenever I have no user. However, it's pretty important to type it as string, If I were to not define this interface, useReducer would infer my State type from the following initial state

const initialState = {
    isLogged: false,
    userID: null,
    username: null,
}

// typeof initialState = {
//  isLogged: boolean;
//  userID: never;
//  username: never;
// }

Enter fullscreen mode Exit fullscreen mode

And would make the TS compiler complain whenever I try to set a valid userID as a string.

Typing the actions

The first step in order for this to be successful, is to type adequately all the actions that the reducer might consume. This is pretty important, as using string literals (specifically in action.type) will help the TS to narrow down the needed payload. In the end, it's only a matter of making a union type between the individual interfaces.


interface UserLoginAction {
    type: "LOGIN";
    payload: yourPayload;
}

interface UserLogoutAction {
    type: "LOGOUT";
}

type UserReducerAction = UserLoginAction | UserLogoutAction;

Enter fullscreen mode Exit fullscreen mode

Writing the reducer

The last part of making this cohesive, is to correctly type the reducer function

const userReducer = (
    state = initialState, 
    // initialState must be explicitly typed as UserState, 
    // in order to state be correctly typed
    action: UserReducerAction
): UserState => {
    switch (action.type) {
        // We get nice intellisense in the case statement too, 
        // which shows us an error if we don't type the type correctly
        case "login":
        // Action gets typed as a UserLoginAction type, 
        // which helps us having the appropriate payload
            return {
                ...state,
                isLogged: true,
                userID: action.payload.id,
                username: action.payload.username,
            };
        case "logout":
        // trying to use action.payload will result in an error
            return {
                ...state,
                isLogged: false,
                userID: null,
                username: null,
            };
        default:
            return state;
    }
};


Enter fullscreen mode Exit fullscreen mode

Conclusion

And there it is, now we have a correctly typed useReducer pattern, which can be extended to, maybe creating Action Creators, or whatever you may like.

Top comments (0)