Redux with Hooks

React.js Redux


HooksHooksclassHooks

  • class
  • componentDidMountcomponentWillUnmountcomponentDidMountcomponentDidUpdateuseEffect
  • this

Hooksreactreact-domHooksReact-Redux

34

connect

import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";

function Form(props) {
    const {
        formId
        formData,
        queryFormData,
        submitFormData,
    } = props;

    useEffect(() => {
        // 请求表单数据
        queryFormData(formId);
    },
        // 指定依赖,防止组件重新渲染时重复请求
        [queryFormData, formId]
    );
  
    // 处理提交
    const handleSubmit = usefieldValues => {
        submitFormData(fieldValues);
    }

    return (
        <FormUI
            data={formData}
            onSubmit={handleSubmit}
        />
    )
}

function mapStateToProps(state) {
    return {
        formData: state.formData
    };
}

function mapDispatchToProps(dispatch, ownProps) {
    // withRouter传入的prop,用于编程式导航
    const { history } = ownProps;

    return {
        queryFormData(formId) {
            return dispatch(queryFormData(formId));
        },
        submitFormData(fieldValues) {
            return dispatch(submitFormData(fieldValues))
            .then(res) => {
                // 提交成功则重定向到主页
                history.push('/home');
            };
        }
    }
}

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(React.memo(Form));

mapDispatchToPropsqueryFormDatauseEffectmapDispatchToPropssubmitFormDatahistorymapStateToPropsformData

mapDispatchToPropsownProps

function mapDispatchToProps(dispatch, ownProps) { // **问题在于这个ownProps!!!**
    const { history } = ownProps;
    ...
}

withRouterhistorymapDispatchToProps

image-20190728144128356

mapDispatchToPropsownPropsmapDispatchTopPropsmapDispatchToPropsqueryFormDatasubmitFormData<Form/>queryFormDatasubmitFormDatauseEffect

// selectorFactory.js
...
// 此函数在connected组件接收到new props时会被调用
function handleNewProps() {
  if (mapStateToProps.dependsOnOwnProps)
    stateProps = mapStateToProps(state, ownProps)
  
  // 声明mapDispatchToProps时如果使用了第二个参数(ownProps)这里会标记为true
  if (mapDispatchToProps.dependsOnOwnProps)
    // 重新调用mapDispatchToProps,更新dispatchProps
    dispatchProps = mapDispatchToProps(dispatch, ownProps)
  
  // mergeProps的做法其实是:mergedProps = { ...ownProps, ...stateProps, ...dispatchProps }
  // 最后传入被connect包裹的组件
  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  return mergedProps
}
...

function Form(props) {
    const {
        formId,
        queryFormData,
        ...
    } = props;

    useEffect(() => {
        // 请求表单数据
        queryFormData(formId);
    },
        // 传入空数组,起到类似componentDidMount的效果
        []
    );
  
    ...
}

useEffectuseEffectcomponentDidMountqueryFormDataformIdHooks FAQeslint-plugin-react-hooks

ownProps

function Form(props) {
    const {
        formId
        queryFormData,
        submitFormData,
        history
        ...
    } = props;

    useEffect(() => {
        queryFormData(formId);
    },
        // 由于声明mapDispatchToProps时没使用ownProps,所以queryFormData是稳定的
        [queryFormData, formId]
    );
  
    const handleSubmit = fieldValues => {
        submitFormData(fieldValues)
          // 把需要用到ownProps的逻辑迁移到组件内定义(使用了redux-thunk中间件,返回Promise)
          .then(res => {
            history.push('/home');
          });
    }

    ...
}

...

function mapDispatchToProps(dispatch) { // 不再声明ownProps参数
    return {
        queryFormData(formId) {
            return dispatch(queryFormData(formId));
        },
        submitFormData(fieldValues) {
            return dispatch(submitFormData(fieldValues));
        }
    }
}

...

mapDispatchToPropsmapDispatchToPropsownPropsmapStateToProps

mapDispatchToProps

connectmapDispatchToPropsdispatchdispatch

...
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";

function Form(props) {
    const {
        formId
        history,
        dispatch
        ...
    } = props;

    useEffect(() => {
        // 在组件内使用dispatch
        // 注意这里的queryFormData来自import,而非props,不会变,所以不用写进依赖数组
        dispatch(queryFormData(formId))
    },
        [dispatch, formId]
    );
  
    const handleSubmit = fieldValues => {
        // 在组件内使用dispatch
        dispatch(submitFormData(fieldValues))
          .then(res => {
            history.push('/home');
          });
    }

    ...
}

...
// 不传入mapDispatchToProps
export default withRouter(connect(mapStateToProps, null)(React.memo(Form));

useEffectuseCallback

import { actionCreator1 } from "@/data/actionCreator1/action";
import { actionCreator2 } from "@/data/actionCreator2/action";
import { actionCreator3 } from "@/data/actionCreator3/action";

...
function Form(props) {
    const {
        dep1,
        dep2,
        dep3,
        dispatch
        ...
    } = props;
  
    // 利用useCallback把useEffect要使用的函数抽离到外部
    const fetchUrl1() = useCallback(() => {
      dispatch(actionCreator1(dep1));
        .then(res => {...})
        .catch(err => {...});
    }, [dispatch, dep1]); // useCallback的第二个参数跟useEffect一样,是依赖项

    const fetchUrl2() = useCallback(() => {
      dispatch(actionCreator2(dep2));
        .then(res => {...})
        .catch(err => {...});
    }, [dispatch, dep2]);

    const fetchUrl3() = useCallback(() => {
      dispatch(actionCreator3(dep3));
        .then(res => {...})
        .catch(err => {...});
    }, [dispatch, dep3]);

    useEffect(() => {
      fetchUrl1();
      fetchUrl2();
      fetchUrl3();
    },
      // useEffect的直接依赖变成了useCallback包裹的函数
      [fetchUrl1, fetchUrl2, fetchUrl3]
    );

    // 为了避免子组件发生不必要的re-render,handleSubmit其实也应该用useCallback包裹
    const handleSubmit = useCallback(fieldValues => {
        // 在组件内使用dispatch
        dispatch(submitFormData(fieldValues))
          .then(res => {
            history.push('/home');
          });
    });

    return (
        <FormUI
            data={formData}
            onSubmit={handleSubmit}
        />
    )
}
...

useCallbackuseEffectuseCallbackuseEffect

mapStateToPropsownPropsmapStateToProps

// 此函数在connected组件接收到new props时会被调用
function handleNewProps() {
  // 声明mapStateToProps时如果使用了ownProps参数同样会产生新的stateProps!
  if (mapStateToProps.dependsOnOwnProps)
    stateProps = mapStateToProps(state, ownProps)
  
  if (mapDispatchToProps.dependsOnOwnProps)
    dispatchProps = mapDispatchToProps(dispatch, ownProps)

  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
  return mergedProps
}

useEffect

v7.1.0

import { useSelector, useDispatch } from 'react-redux'

// selector函数的用法和mapStateToProps相似,其返回值会作为useSelector的返回值,但与mapStateToProps不同的是,前者可以返回任何类型的值(而不止是一个对象),此外没有第二个参数ownProps(因为可以在组件内通过闭包拿到)
const result : any = useSelector(selector : Function, equalityFn? : Function)
const dispatch = useDispatch()

...
import { useSelector, useDispatch } from "react-redux";
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";

function Form(props) {
    const {
        formId
        history,
        dispatch
        ...
    } = props;
  
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(queryFormData(formId))
    },
        [dispatch, formId]
    );
  
    const handleSubmit = useCallback(fieldValues => {
        dispatch(submitFormData(fieldValues))
          .then(res => {
            history.push('/home');
          });
    }, [dispatch, history]);

    const formData = useSelector(state => state.formData;);
  
    ...

    return (
        <FormUI
            data={formData}
            onSubmit={handleSubmit}
        />
    );
}

...

// 无需使用connect
export default withRouter(React.memo(Form));

"不使用mapDispatchToProps"dispatchdispatchstatemapStateToPropsuseSelector

useSelector===useSelectorreselectuseSelectorshallowEqual

import { useSelector, shallowEqual } from 'react-redux'

const selector = state => ({
  a: state.a,
  b: state.b
});

const data = useSelector(selector, shallowEqual);

HooksHooks

  • HooksuseContextuseReducer

  • import { createContext, useContext, useReducer, memo } from 'react';
    
    function reducer(state, action) {
        switch (action.type) {
            case 'UPDATE_HEADER_COLOR':
              return {
                  ...state,
                  headerColor: 'yellow'
              };
            case 'UPDATE_CONTENT_COLOR':
              return {
                  ...state,
                  contentColor: 'green'
              };
            default:
              break;
        }
    }
    
    // 创建一个context
    const Store = createContext(null);
    // 作为全局state
    const initState = {
        headerColor: 'red',
        contentColor: 'blue'
    };
    
    const App = () => {
        const [state, dispatch] = useReducer(reducer, initState);
    		// 在根结点注入全局state和dispatch方法
        return (
          <Store.Provider value={{ state, dispatch }}>
            <Header/>
            <Content/>
          </Store.Provider>
        );
    };
    
    const Header = memo(() => {
      	// 拿到注入的全局state和dispatch
        const { state, dispatch } = useContext(Store);
        return (
        	<header
          	style={{backgroundColor: state.headerColor}}
            onClick={() => dispatch('UPDATE_HEADER_COLOR')}
          />
        );
    });
    
    const Content = memo(() => {
        const { state, dispatch } = useContext(Store);
        return (
        	<div
            style={{backgroundColor: state.contentColor}}
            onClick={() => dispatch('UPDATE_CONTENT_COLOR')}
          />
        );
    });
    

contextstateactionsdispatchProvider

<Header/><Content/>

<Header/><Content/>

memo

memostateuseContextmemo

  • context

  • memo

    const Header = () => {
        const { state, dispatch } = useContext(Store);
        return memo(<ThemedHeader theme={state.headerColor} dispatch={dispatch} />);
    };
    
    const ThemedHeader = memo(({theme, dispatch}) => {
        return (
            <header
                style={{backgroundColor: theme}}
                onClick={() => dispatch('UPDATE_HEADER_COLOR')}
            />
        );
    });
    
  • useMemo hook

    const Header = () => {
        const { state, dispatch } = useContext(Store);
        return useMemo(
            () => (
                <header
                    style={{backgroundColor: state.headerColor}}
                    onClick={() => dispatch('UPDATE_HEADER_COLOR')}
            		/>
            ),
            [state.headerColor, dispatch]
        );
    };
    

Context + Hooks

connectownProps