【问题标题】:How to connect a Higher-Order Component to a Redux store?如何将高阶组件连接到 Redux 商店?
【发布时间】:2019-12-14 14:29:58
【问题描述】:

基本上,我有一个AuthenticationHOC,它必须获取 redux 状态,检查令牌是否存在,如果存在,则渲染包装的组件。如果不是,则派发一个操作以尝试从 localStorage 加载令牌。如果失败,请重定向到登录页面。

import React from 'react';
import { connect } from 'react-redux';
import * as UserActions from '../../state/actions/user-actions';
import * as DashboardActions from '../../state/actions/dashboard-actions';

const mapStateToProps = state => {
  return {
    token: state.user.token,
    tried: state.user.triedLoadFromStorage,
  };
};

const _AuthenticationHOC = Component => props => {
  // if user is not logged and we 've not checked the localStorage
  if (!props.token && !props.tried) {
    // try load the data from local storage
      props.dispatch(DashboardActions.getDashboardFromStorage());
      props.dispatch(UserActions.getUserFromStorage());
  } else {
    // if the user has not token or we tried to load from localStorage 
    //without luck, then redirect to /login
    props.history.push('/login');
  }

  // if the user has token render the component
  return <Component />;
};

const AuthenticationHOC = connect(mapStateToProps)(_AuthenticationHOC);
export default AuthenticationHOC;

然后我尝试像这样使用它

const SomeComponent = AuthenticationHOC(connect(mapStateToProps)(HomeComponent));

但我总是得到一个错误标记,正是上面的行。

TypeError: Object(...) is not a function

然后我做了一个简化版

我将 HOC 中的代码替换为最简单的版本

const _AuthenticationHOC = Component => props => {
  return <Component {...props}/>;
};

这也不起作用。然后我从我的 HOC 中删除了连接功能,然后只导出这个组件和 tada! ...现在可以使用了!

所以我怀疑 connect 返回的对象不能用作 HoC 函数。它是否正确?我可以在这里做什么?

【问题讨论】:

    标签: javascript reactjs redux react-redux higher-order-components


    【解决方案1】:

    请参阅此答案的底部以阅读对问题内容的直接回复。我将从我们在日常开发中使用的良好实践开始。


    连接Higher-Order Component

    Redux 提供了一个有用的compose utility function

    compose 所做的只是让您编写深度嵌套的函数转换,而无需代码向右漂移。

    所以在这里,我们可以使用它来嵌套 HoC,但以一种可读的方式。

    // Returns a new HoC (function taking a component as a parameter)
    export default compose(
      // Parent HoC feeds the Auth HoC
      connect(({ user: { token, triedLoadFromStorage: tried } }) => ({
        token,
        tried
      })),
    
      // Your own HoC
      AuthenticationHOC
    );
    

    这类似于手动创建新的容器 HoC 函数。

    const mapState = ({ user: { token, triedLoadFromStorage: tried } }) => ({
        token,
        tried
    });
    
    const withAuth = (WrappedComponent) => connect(mapState)(
      AuthenticationHOC(WrappedComponent)
    );
    
    export default withAuth;
    

    然后,您可以透明地使用您的 auth HoC。

    import withAuth from '../AuthenticationHOC';
    // ...
    export default withAuth(ComponentNeedingAuth);
    

    编写干净且可测试的 HoC

    为了将身份验证组件与存储和路由隔离,我们可以将其拆分为多个文件,每个文件都有自己的职责。

    - withAuth/
      - index.js           // Wiring and exporting (container component)
      - withAuth.jsx       // Defining the presentational logic
      - withAuth.test.jsx  // Testing the logic
    

    我们将withAuth.jsx 文件的重点放在渲染和逻辑上,无论它来自何处。

    // withAuth/withAuth.jsx
    import React from 'react';
    
    const withAuth = (WrappedComponent) => ({
      // Destructure props here, which filters them at the same time.
      tried,
      token,
      getDashboardFromStorage, 
      getUserFromStorage, 
      onUnauthenticated, 
      ...props
    }) => {
      // if user is not logged and we 've not checked the localStorage
      if (!token && !tried) {
        // try load the data from local storage
        getDashboardFromStorage();
        getUserFromStorage();
      } else {
        // if the user has no token or we tried to load from localStorage
        onUnauthenticated();
      }
    
      // if the user has token render the component PASSING DOWN the props.
      return <WrappedComponent {...props} />;
    };
    
    export default withAuth;
    

    看到了吗?我们的 HoC 现在不知道存储和路由逻辑。我们可以将重定向移动到 store 中间件或其他任何地方,如果 store 不是您想要的位置,它甚至可以在 prop &lt;Component onUnauthenticated={() =&gt; console.log('No token!')} /&gt; 中自定义。

    那么,我们只提供index.js中的props,就像一个容器组件。1

    // withAuth/index.js
    import React from 'react';
    import { connect } from 'react-redux';
    import { compose } from 'redux';
    import { getDashboardFromStorage, onUnauthenticated } from '../actions/user-actions';
    import { getUserFromStorage } from '../actions/dashboard-actions';
    import withAuth from './withAuth';
    
    export default compose(
      connect(({ user: { token, triedLoadFromStorage: tried } }) => ({
        token,
        tried
      }), {
        // provide only needed actions, then no `dispatch` prop is passed down.
        getDashboardFromStorage,
        getUserFromStorage,
        // create a new action for the user so that your reducers can react to
        // not being authenticated
        onUnauthenticated,
      }),
    
      withAuth
    );
    

    onUnauthenticated 作为存储操作的好处是不同的 reducer 现在可以对其做出反应,例如擦除用户数据、重置仪表板数据等。

    测试 HoC

    然后,可以使用Jestenzyme 之类的东西来测试withAuth HoC 的隔离逻辑。

    // withAuth/withAuth.test.jsx
    import React from 'react';
    import { mount } from 'enzyme';
    import withAuth from './withAuth';
    
    describe('withAuth HoC', () => {
      let WrappedComponent;
      let onUnauthenticated;
    
      beforeEach(() => {
        WrappedComponent = jest.fn(() => null).mockName('WrappedComponent');
        // mock the different functions to check if they were called or not.
        onUnauthenticated = jest.fn().mockName('onUnauthenticated');
      });
    
      it('should call onUnauthenticated if blah blah', async () => {
        const Component = withAuth(WrappedComponent);
        await mount(
          <Component 
            passThroughProp
            onUnauthenticated={onUnauthenticated} 
            token={false}
            tried
          />
        );
    
        expect(onUnauthenticated).toHaveBeenCalled();
    
        // Make sure props on to the wrapped component are passed down
        // to the original component, and that it is not polluted by the
        // auth HoC's store props.
        expect(WrappedComponent).toHaveBeenLastCalledWith({
          passThroughProp: true
        }, {});
      });
    });
    

    为不同的逻辑路径添加更多测试。


    关于你的情况

    所以我怀疑connect返回的对象不能用作HoC函数。

    react-redux 的 connect returns an HoC.

    import { login, logout } from './actionCreators'
    
    const mapState = state => state.user
    const mapDispatch = { login, logout }
    
    // first call: returns a hoc that you can use to wrap any component
    const connectUser = connect(
      mapState,
      mapDispatch
    )
    
    // second call: returns the wrapper component with mergedProps
    // you may use the hoc to enable different components to get the same behavior
    const ConnectedUserLogin = connectUser(Login)
    const ConnectedUserProfile = connectUser(Profile)
    

    在大多数情况下,包装函数会立即被调用,而不需要 保存在临时变量中:

    export default connect(mapState, mapDispatch)(Login)
    

    然后我尝试像这样使用它

    AuthenticationHOC(connect(mapStateToProps)(HomeComponent))
    

    您已经接近了,尽管您连接 HoC 的顺序颠倒了。应该是:

    connect(mapStateToProps)(AuthenticationHOC(HomeComponent))
    

    这样,AuthenticationHOC 接收来自 store 的 props,HomeComponent 被正确的 HoC 正确包装,这将返回一个新的有效组件。

    话虽如此,我们可以做很多事情来改进这个 HoC!


    1.如果您不确定是否将 index.js 文件用于容器组件,您可以随意重构它,比如导出一个 withAuthContainer.jsx 文件索引或让开发人员选择他们需要的索引。

    【讨论】:

    • 感谢您的详细回答,向更多经验丰富的开发人员学习如何编写更好的代码总是很棒的。
    【解决方案2】:

    connect()中所述:

    connect() 的返回是一个包装函数,它将你的 组件并返回一个带有附加道具的包装器组件 注入。

    因此,我们可以将序列反转为:

    const AuthenticationHOC = _AuthenticationHOC(HomeComponent);
    export default connect(mapStateToProps)(AuthenticationHOC);
    

    并确保在您的 HOC 中传递 props

    const _AuthenticationHOC = Component => props => {
      return <Component {...props} />; // pass props
    };
    

    【讨论】:

      【解决方案3】:

      正如您的第一次尝试所描述的: const SomeComponent = AuthenticationHOC(connect(mapStateToProps)(HomeComponent))

      根据定义,这意味着将参数作为纯组件传递给AuthenticationHOC 将返回另一个组件。但是在这里你传递了另一个 HOC,即connect(),它不是一个组件,而是一个包装器。所以根据定义,return &lt;Component /&gt; 解析为return &lt;connect(mapStateToProps) /&gt; 将产生语法错误或运行时错误。

      将纯组件作为HomeComponent 传递将起作用,因为它只是一个组件。

      我的猜测是,connect() 在幕后是 currying。它的作用是返回一个组件包装器,并将其 mapStateToPropsmapDispatchToProps 作为注入的附加道具。来源 - https://react-redux.js.org/api/connect#connect-returns

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-04-02
        • 1970-01-01
        • 2019-07-13
        • 1970-01-01
        • 1970-01-01
        • 2020-06-09
        • 1970-01-01
        相关资源
        最近更新 更多