Using typescript to write react-redux and redux-thunk, and the implementation of thunk and other middleware

Keywords: Javascript React TypeScript Attribute JSON

General steps for using react-redux

  • Provider, as the provider of the top-level global state, needs to pass a parameter, the global state store
import { Provider } from 'react-redux';
<Provider store={ store }></Provider>
  • Stores are created by the createStore function and need to pass the reducer pure function as a parameter
import { createStore, combineReducers } from 'redux';
const rootReducer = combineReducers({
    reducer
});
const store = createStore(rootReducer);
  • Reducr takes two more parameters, state and action, and returns a new state based on different actions
const reducer = (state, action) => {
    switch (action) {
        default:
            return state;
    }
};
  • Now you can use redux in your components
  • mapStateToProps receives a global state as a parameter, returns an object with properties as props to the component
{ props.reducer };

const mapStateToProps = (state) => ({
    reducer: state.reducer
})
  • mapDispatchToProps receives dispatch as a parameter and returns the object's property as a method that calls it to update, passing either the object (which can trigger dispatch only once) or the function (which can trigger any number of times, and of course an asynchronous operation can)
props.action();

const mapDispatchToProps = (dispatch) => ({
    action: () => dispatch(action)
})
  • ActionCreator is used to generate parameters passed to dispatch, that is, action objects are usually returned in actionCreator
// Return Object
const actionCreator = function(data) {
    return {
        type: ACTION_TYPE,
        data
    }
}

// Use
props.action(someData);

const mapDispatchToProps = (dispatch) => ({
    action: (data) => dispatch(actionCreator(data))
})

TypeScript Writing

The above code executes well in the js environment. Change the suffix of the file name to.ts directly, without surprise it will prompt an error...

For example:

// action.ts
// error: parameter "data" implicitly has "any" type
export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data) => ({
    type: CHANGE_NAME,
    data
});

// reducer.ts
// error: The parameter'action'implicitly has an'any' type
import { CHANGE_NAME } from './action';
const initialState = {
    name: 'lixiao'
};
export default (state = initialState, action) => {
    switch (action.type) {
        case CHANGE_NAME:
            retutn Object.assign({}, state, {
                name: action.data
            });
        default:
            return state;
    }
};

// rootReducer.ts
import { combineReducers } from 'redux';
import home from './pages/Home/reducer';
export default combineReducers({
    home
});

// Home.tsx
// error: The parameter'state'implicitly has an'any' type
//        The parameter'dispatch'implicitly has an'any' type
//        The parameter "data" implicitly has an "any" type
const mapStatetoProps = (state) => ({
    name: state.home.name
});
const mapDispatchToProps = (dispatch) => ({
    changeName: (data) => dispatch(changeName(data)),
})

ts is compiled statically, in part to avoid such operations as taking attributes at will.For example, operations such as state.home in mapStateToProps need to tell ts which attributes of the state in the parameter are preferable, and similarly, what attributes are preferable for each reducer after splitting, they need to be explicitly told to ts (it's not very friendly to use any type).Next, type declarations are made for the variables that prompt errors.

actionCreator

data in a parameter is used as a load for this type of action, and it is acceptable to set the variable type to any

// action.ts
export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data: any) => ({
    type: CHANGE_NAME,
    data
});

reducer

Reducr receives state and action parameters. Actions usually have an action type attribute, type, and action payload data, which can be unified throughout the application. While the state here is accessed in other components connected to redux, it is necessary to define an interface to specify which properties of the state can be accessed.

// global.d.ts
declare interface IAction {
    type: string;
    data: any;
}

// reducer.ts
import { CHANGE_NAME } from './action';

// This interface convention has properties accessible to the child reducer
export interface IHomeState {
    name: string;
}
const initialState: IHomeState = {
    name: 'lixiao'
};
export default (state = initialState, action: IAction): IHomeState => {
    switch (action.type) {
        case CHANGE_NAME:
            return Object.assign({}, state, {
                name: action.data
            });
        default:
            return state;
    }
};

rootReducer

As the top-level state, it is often used in subcomponents to access the state in the child reducer, which attributes can be retrieved by the state in the child reducer and which sub-states can be retrieved by the top-level state.

// rootReducer.ts
import { combineReducers } from 'redux';
import home, { IHomeState } from './pages/Home/reducer';

// Each additional child reducer synchronizes updates in this interface
export interface IStore {
    home: IHomeState
}
export default combineReducers({
    home
});

Using redux in components

// Home.tsx
import { Dispatch } from 'redux';
import { IStore } from '../../rootReducer';

// props type of component
interface IProps {
    name: string;
    // The changeName and actionCreator are not the same here, so the return value type is not an object
    changeName(data: any): void;
}
const mapStatetoProps = (state: IStore) => ({
    name: state.home.name
});
// Dispatch receives parameters as objects
const mapDispatchToProps = (dispatch: Dispatch) => ({
    changeName: (data: any) => dispatch(changeName(data)),
})

redux-thunk

In the previous example, an action is passed to dispatch and a new state is returned from the reducer function, which is synchronous, similar to a mutation in vuex.redux-thunk is a solution for handling asynchronous actions.

Asynchronous action

A common scenario is when dispatch is triggered after an ajax request is successfully sent.The simplest way to do this is to encapsulate the operation as a function:

const asyncAction = () => {
    dispatch({ type, data });
    ajax().then(res => dispatch(res));
}

What if this operation needs to be called in more than one place?Copying and pasting this function is also possible, but it is better to use a middleware such as redux-thunk to generate an action in actionCreator that contains asynchronous operations.

redux-thunk usage

  • Pass in redux-thunk middleware when creating store
// index.tsx
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));
  • actionCreator returns a function in which you can do anything, trigger dispatch es any number of times, including asynchronous operations.
// action.ts
import { Dispatch } from 'redux';

export const CHANGE_NAME = 'CHANGE_NAME';
export const changeName = (data: any) => ({
    type: CHANGE_NAME,
    data
});
export const changeNameAsync = (data?: any) => (dispatch: Dispatch) => {
    dispatch(changeName('loading'));
    fetch('/api').then(res => res.json()).then(res => dispatch(changeName(res.data)));
}
  • Use the action of this return function in the component
// Home.tsx
import { changeName, changeNameAsync } from './action';

// Dispatch can pass in objects and functions, but you can't simply use the Dispatch type here
const mapDispatchToProps = (dispatch: any) => ({
    changeName: (data: any) => dispatch(changeName(data)),
    changeNameAsync: () => dispatch(changeNameAsync())
});
// You can also use bindActionCreators
const mapDispatchToProps = (dispatch: Dispatch) => (bindActionCreators({
    changeName,
    changeNameAsync
}, dispatch))

redux-thunk implementation process

The difference between a synchronous action and an asynchronous action is that a dispatch object is operated synchronously, while an asynchronous operation is a function of dispatch, which can contain any operation such as asynchronous, dispatch synchronous action, and so on.

createStore

Base Version

Before redux-thunk was introduced, store s were generated in the following way

const store = createrStore(reducer, initialState)

The second parameter, initialState, is the initial state, optional

// Two parameters
function createStore(reducer, preloadedState) {
    let currentReducer = reducer;
    let currentState = preloadedState;
    
    // More important function dispatch
    function dispatch(action) {
        // This is why you can also initialize state in reducer
        // You can also find that the preloadedState parameter here takes precedence over the initialState in reducer
        currentState = currentReducer(currentState, action);
    }
    
    // When the page just opens, call createStore, execute dispatch again, and update the currentState
    // So there's an action of type @@INIT in the redux development tool
    dispatch({ type: ActionTypes.INIT });
}
Advanced Version

With the introduction of redux-thunk, the way store s are generated has also changed

const store = createrStore(reducer, initialState, enhancer)

The third parameter is a function, so let's see what happens inside createStore when the third parameter is passed in

function createStore(reducer, preloadedState, enhancer) {
    // Since the second parameter is optional, some code is required to detect the number and type of parameters
    // ts is convenient...
    // The detection passed before it was return ed.
    return enhancer(createStore)(reducer, preloadedState)
}

Do a code conversion

const store = createStore(rootReducer, applyMiddleware(thunk));
// enhancer is applyMiddleware(thunk)
// preloadedState is undefined
// Code Conversion
const store = applyMiddleware(thunk)(createStore)(rootReducer);

applyMiddleware

import compose from './compose';

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

Next to code swapping

const store = applyMiddleware(thunk)(createStore)(rootReducer);
// Equivalent to executing the following sequence of codes
const store = createStore(rootReducer) // This store is in the base version
let dispatch = () => {
    throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
        'Other middleware would not be applied to this dispatch.'
    )
}

const middlewareAPI = {
    getState: store.getState,
    // dispatch: (rootReducer) => dispatch(rootReducer)
    dispatch: dispatch
}
const chain = [thunk].map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
// This is the store that eventually returns 
return {
    ...store,
    dispatch
}

Take another look at what thunk (middleware API), compose did

thunk

// After redux-thunk/src/index.js simplification
const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState);
    }

    return next(action);
};

compose

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

Multiple middleware passed in, which in turn takes a parameter; only one middleware returns this function directly

How dispatch was expanded

const store = createStore(rootReducer)
// const chain = [thunk].map(middleware => middleware(middlewareAPI))
// const chain = [thunk(middlewareAPI)];
const chain = [(next => action => {
    if (typeof action === 'function') {
        return action(dispatch, store.getState);
    }
    return next(action);
})]
// dispatch = compose(...chain)(store.dispatch)
dispatch = action => {
    if (typeof action === 'function') {
        return action(dispatch, store.getState);
    }
    return store.dispatch(action);
}

When a parameter passed to dispatch is either a function or a base version, call the reducer function directly to update the state; when a function is passed in, execute the contents of the function body and pass dispatch in, and within this function you can use dispatch to do what you want.

summary

Most functions are provided by redux, and closures are used more often, (params1) => (params2) => {someFunction (params1, params2)} which can be used a lot and seem dizzy, but it is also the function that is executed to pass in parameters once.

Posted by amberb617 on Sat, 09 Nov 2019 22:09:38 -0800