Set Up a Typescript React Redux Project

栏目: IT技术 · 发布时间: 4年前

内容简介:This post provides a way to type your React Redux project with Typescript.This post loosely uses the

Set Up a Typescript React Redux Project

Introduction

This post provides a way to type your React Redux project with Typescript.

Using the Ducks Pattern

This post loosely uses the Redux Ducks proposal, which groups Redux “modules” together rather than by functionality in Redux. For example, all of the Redux code related to the users piece of state lives in the same file rather than being scattered across different types , actions , and reducer folders throughout your app. If this isn’t quite clear yet, you’ll see what I mean shortly!

Example App

As an example, let’s pretend we’re making a shopping cart app where we have a user that may or may not be logged in and we have products . These will serve as the two main parts of Redux state.

Since we’re focused on Redux typings, let’s bootstrap our app using create-react-app so we can get up and running quickly. Remember to give it the --typescript flag when you create the project.

yarn create react-app shopping-cart --typescript

Great! Now, let’s go into our app directory and instal Redux and its types.

yarn add redux react-redux @types/redux @types/react-redux

Setting Up our First Module

Let’s create the user module. We’ll do this by creating a src/redux/modules/user.ts file. We can define our UserState type and a couple action creators: login and logout .

Since we’re not going to worry about validating passwords, we can just assume we only have a username prop on our user state that can either be a string for a logged-in user or null for a guest.

src/redux/modules/user.ts

type UserState = {
  username: string | null;
};

const initialState: UserState = { username: null };

const login = (username: string) => ({
  type: 'user/LOGIN';
  payload: username;
});

const logout = () => ({
  type: 'user/LOGOUT'
});

Note that the user/login is a rough adaptation of the Redux Ducks proposal to name your types in the format app-name/module/ACTION .

Next, let’s create a user reducer. A reducer takes the state and an action and produces a new state. We know we can type both our state argument and the reducer return value as UserState , but how should we type the action we pass to the reducer? Our first approach will be taking the ReturnType of the login and logout action creators.

src/redux/modules/user.ts

type UserState = {
  username: string | null;
};

const initialState: UserState = { username: null };

const login = (username: string) => ({
  type: 'user/LOGIN',
  payload: username,
});

const logout = () => ({
  type: 'user/LOGOUT',
});

type UserAction = ReturnType<typeof login | typeof logout>;

export function userReducer(
  state = initialState,
  action: UserAction
): UserState {
  switch (action.type) {
    case 'user/LOGIN':
      return { username: action.payload };
    case 'user/LOGOUT':
      return { username: null };
    default:
      return state;
  }
}

Unfortunately, we have a couple problems. First, we’re getting the following Typescript compilation error: Property 'payload' does not exist on type '{ type: string; }' . This is because our attempted union type isn’t quite working and the Typescript compiler thinks we may or may not have an action payload for the login case.

The second issue, which turns out to cause the first issue, is that the Typescript compiler doesn’t detect an incorrect case in our switch statement. For example, if added a case for "user/UPGRADE" , we would want an error stating that it’s not an available type.

How do we solve these issues?

Function Overloads and Generics to the Rescue!

It turns out we can solve this issue by using Typescript function overloads and generics . What we’ll do is make a function that creates typed actions for us. The type created by this function will be a generic that extends string . The payload will be a generic that extends any .

src/redux/modules/user.ts

export function typedAction<T extends string>(type: T): { type: T };
export function typedAction<T extends string, P extends any>(
  type: T,
  payload: P
): { type: T; payload: P };
export function typedAction(type: string, payload?: any) {
  return { type, payload };
}

type UserState = {
  username: string | null;
};

const initialState: UserState = { username: null };

export const login = (username: string) => {
  return typedAction('user/LOGIN', username);
};

export const logout = () => {
  return typedAction('user/LOGOUT');
};

type UserAction = ReturnType<typeof login | typeof logout>;

export function userReducer(
  state = initialState,
  action: UserAction
): UserState {
  switch (action.type) {
    case 'user/LOGIN':
      return { username: action.payload };
    case 'user/LOGOUT':
      return { username: null };
    default:
      return state;
  }
}

Success! We’re now free of our compilation errors. Even better, we can be sure our cases are restricted to actual types we’ve created.

Set Up a Typescript React Redux Project

Creating Our RootReducer and Store

Now that we have our first module put together, let’s create our rootReducer in the src/redux/index.ts file.

src/redux/index.ts

import { combineReducers } from 'redux';
import { userReducer } from './modules/user';

export const rootReducer = combineReducers({
  user: userReducer,
});

export type RootState = ReturnType<typeof rootReducer>;

If you’re familiar with Redux, this should look pretty standard to you. The only slightly unique piece is that we’re exporting a RootState using the ReturnType of our rootReducer .

Next, let’s create our store in index.tsx and wrap our app in a Provider . Again, we should be familiar with this if we’re familiar with Redux.

src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './redux';

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Adding a Module with Thunks

Often, we’ll need some async functionality in our action creators. For example, when we get a list of products , we’ll probably be performing a fetch request that will resolve its Promise at some future time.

To allow for this asynchronous functionality, let’s add redux-thunk and its types, which lets us return thunks from our action creators.

yarn add redux-thunk @types/redux-thunk

Next, let’s make sure to add this middleware when creating our store .

src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { rootReducer } from './redux';
import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Great! We can now create our products module, which will have the ability to return thunks from its action creators.

The product piece of our state will be a litte more complicated. It’ll have a products prop, a cart prop, and a loading prop.

src/redux/modules/products.ts

// TODO: We should move typedAction elsewhere since it's shared
import { typedAction } from './users';
import { Dispatch, AnyAction } from 'redux';
import { RootState } from '..';

type Product = {
  id: number;
  name: string;
  price: number;
  img: string;
};

type CartItem = {
  id: number;
  quantity: number;
};

type ProductState = {
  products: Product[];
  loading: boolean;
  cart: CartItem[];
};

const initialState: ProductState = {
  products: [],
  loading: false,
  cart: [],
};

const setProducts = (products: Product[]) => {
  return typedAction('products/SET_PRODUCTS', products);
};

export const addToCart = (product: Product, quantity: number) => {
  return typedAction('products/ADD_TO_CART', { product, quantity });
};

// Action creator returning a thunk!
export const loadProducts = () => {
  return (dispatch: Dispatch<AnyAction>, getState: () => RootState) => {
    setTimeout(() => {
      // Pretend to load an item
      dispatch(
        setProducts([
          ...getState().products.products,
          {
            id: 1,
            name: 'Cool Headphones',
            price: 4999,
            img: 'https://placeimg.com/640/480/tech/5',
          },
        ])
      );
    }, 500);
  };
};

type ProductAction = ReturnType<typeof setProducts | typeof addToCart>;

export function productsReducer(
  state = initialState,
  action: ProductAction
): ProductState {
  switch (action.type) {
    case 'products/SET_PRODUCTS':
      return { ...state, products: action.payload };
    case 'products/ADD_TO_CART':
      return {
        ...state,
        cart: [
          ...state.cart,
          {
            id: action.payload.product.id,
            quantity: action.payload.quantity,
          },
        ],
      };
    default:
      return state;
  }
}

There’s a lot going on here, but the real novelty is in loadProducts , our action creator that returns a thunk. Our setTimeout function is simulating a fetch without having to actually perform a fetch.

We now need to register the productsReducer with our rootReducer . At this point, it’s as easy as adding the respective key.

src/redux/index.ts

import { combineReducers } from 'redux';
import { userReducer } from './modules/user';
import { productsReducer } from './modules/products';

export const rootReducer = combineReducers({
  user: userReducer,
  products: productsReducer,
});

export type RootState = ReturnType<typeof rootReducer>;

Using In Our App

We’re ready to use our Redux store! We’ve already added the Provider to our index.tsx file, so all we have to do is connect individual components.

Let’s first connect an Auth component. We’ll want to access the user.username prop from our state as well as the login and logout action creators.

src/Auth.tsx

import React from 'react';
import { RootState } from './redux';
import { login, logout } from './redux/modules/user';
import { connect } from 'react-redux';

const mapStateToProps = (state: RootState) => ({
  username: state.user.username,
});

const mapDispatchToProps = { login, logout };

type Props = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps;

const UnconnectedAuth: React.FC<Props> = props => {
  // Do auth things here!
  return <>{props.username}</>;
};

export const Auth = connect(
  mapStateToProps,
  mapDispatchToProps
)(UnconnectedAuth);

Note that we define mapStateToProps and mapDispatchToProps at the to, which helps us derive the Props type using ReturnType . We now have access to props.username , props.login , and props.logout in our component.

Dispatching Thunks

One wrinkle is when we want to map in an action creator that returns a thunk. We can use map in our loadProducts action creator as an example. In this case, we use Redux’s handy bindActionCreators function!

src/Products.tsx

import React from 'react';
import { RootState } from './redux';
import { loadProducts } from './redux/modules/products';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

const mapStateToProps = (state: RootState) => ({
  cart: state.products.cart,
});

const mapDispatchToProps = (dispatch: Dispatch) => {
  return bindActionCreators(
    {
      loadProducts,
    },
    dispatch
  );
};

type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps>;

const UnconnectedProducts: React.FC<Props> = props => {
  // Do cart things here!
  return <>Your Cart</>;
};

export const Products = connect(
  mapStateToProps,
  mapDispatchToProps
)(UnconnectedProducts);

Conclusion

And that’s it! Not too bad to get the state management goodness of Redux with the type safety of Typescript. If you want to see a similar app in action, please check out the associated github repo .


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

刷新

刷新

[美] 萨提亚·纳德拉 / 陈召强、杨洋 / 中信出版集团 / 2018-1 / 58

《刷新:重新发现商业与未来》是微软CEO萨提亚•纳德拉首部作品。 互联网时代的霸主微软,曾经错失了一系列的创新机会。但是在智能时代,这家科技公司上演了一次出人意料的“大象跳舞”。2017年,微软的市值已经超过6000亿美元,在科技公司中仅次于苹果和谷歌,高于亚马逊和脸谱网。除了传统上微软一直占有竞争优势的软件领域,在云计算、人工智能等领域,微软也获得强大的竞争力。通过收购领英,微软还进入社交......一起来看看 《刷新》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具