简体   繁体   English

如何将高阶组件连接到 Redux 存储?

[英]How to connect a Higher-Order Component to a Redux store?

Basically, I have an AuthenticationHOC which must get the redux state, check if a token exists and if it exists, render the wrapped component.基本上,我有一个AuthenticationHOC必须获取 redux 状态,检查令牌是否存在,如果存在,则呈现包装的组件。 If not, then dispatch an action to try and load the token from the localStorage.如果没有,则调度一个动作来尝试从 localStorage 加载令牌。 If this fails, redirect to the login page.如果失败,请重定向到登录页面。

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;

then I tried to use this like this然后我尝试像这样使用它

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

but I always get an error marking exactly the line above.但我总是收到一个错误,准确标记了上面的行。

TypeError: Object(...) is not a function类型错误:Object(...) 不是函数

then I did a simplified version然后我做了一个简化版

I replaced the code from my HOC to the most simple version我将 HOC 中的代码替换为最简单的版本

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

and this doesn't work either.这也不起作用。 Then I removed the connect function from my HOC and just export this component and tada!然后我从我的 HOC 中删除了连接功能,然后只导出这个组件和 tada! ...works now! ...现在工作!

So I suspect that connect returns an object that can't be used as a HoC function.所以我怀疑 connect 返回了一个不能用作 HoC 函数的对象。 Is this correct?这样对吗? What could I do here?我能在这里做什么?

See the bottom of this answer to read a direct response to the question's content.请参阅此答案的底部以阅读对问题内容的直接回复。 I'll start with good practices we use in our everyday development.我将从我们在日常开发中使用的良好实践开始。


Connecting an Higher-Order Component连接高阶组件

Redux offers a useful compose utility function . Redux 提供了一个有用的compose实用程序函数

All compose does is let you write deeply nested function transformations without the rightward drift of the code. compose所做的只是让您编写深度嵌套的函数转换,而无需代码向右漂移。

So here, we can use it to nest HoCs but in a readable way.因此,在这里,我们可以使用它以可读的方式嵌套 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
);

Which would be similar to manually creating a new container HoC function.这类似于手动创建一个新的容器 HoC 函数。

const mapState = ({ user: { token, triedLoadFromStorage: tried } }) => ({
    token,
    tried
});

export default WrappedComponent => connect(mapState)(
  AuthenticationHOC(WrappedComponent)
);

Then, you can use your auth HoC transparently.然后,您可以透明地使用您的 auth HoC。

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

Writing a clean and testable HoC编写一个干净且可测试的 HoC

In order to isolate the auth component from the store and the routing, we could split it in multiple files, each with its own responsibility.为了将 auth 组件与存储和路由隔离,我们可以将其拆分为多个文件,每个文件都有自己的职责。

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

We keep the withAuth.jsx file focused on the rendering and the logic, regardless from where it's coming.我们让withAuth.jsx文件专注于渲染和逻辑,无论它来自哪里。

// withAuth/withAuth.jsx
import React from 'react';

export default Component => ({
  // 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 <Component {...props} />;
};

See?看? Our HoC is now unaware of the store and routing logic.我们的 HoC 现在不知道存储和路由逻辑。 We could move the redirection into a store middleware, or anywhere else, it could even be customized in a prop <Component onUnauthenticated={() => console.log('No token!')} /> if the store is not the place you'd like it.我们可以将重定向移动到 store 中间件,或者其他任何地方,它甚至可以在 prop <Component onUnauthenticated={() => console.log('No token!')} />定制,如果 store 不是你喜欢的地方。

Then, we only provide the props in the index.js , like a container component.然后,我们只在index.js提供 props,就像一个容器组件。 1 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
);

The good thing having the onUnauthenticated as a store action is that different reducers could now react to it, like wiping the user data, its dashboard data, etc.onUnauthenticated作为存储操作的onUnauthenticated是不同的 reducer 现在可以对其做出反应,例如擦除用户数据、仪表板数据等。

Testing the HoC测试 HoC

Then, it's possible to test the isolated logic of the withAuth HoC with something like Jest and enzyme .然后,它是可以测试的分离逻辑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
    }, {});
  });
});

Add more tests for different logical paths.为不同的逻辑路径添加更多测试。


About your situation关于你的情况

So I suspect that connect returns an object that can't be used as a HoC function.所以我怀疑connect返回了一个不能用作 HoC 函数的对象。

react-redux's connect returns an HoC . react-redux 的connect返回一个 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)

In most cases, the wrapper function will be called right away, without being saved in a temporary variable:在大多数情况下,包装器函数将被立即调用,而不会保存在临时变量中:

 export default connect(mapState, mapDispatch)(Login)

then I tried to use this like this然后我尝试像这样使用它

AuthenticationHOC(connect(mapStateToProps)(HomeComponent))

You were close, though the order in which you wired the HoCs is reversed.虽然您连接 HoC 的顺序颠倒了,但您已经接近了。 It should be:它应该是:

connect(mapStateToProps)(AuthenticationHOC(HomeComponent))

This way, the AuthenticationHOC receives the props from the store and HomeComponent is correctly wrapped by the right HoC, which would return a new valid component.这样一来, AuthenticationHOC从商店收到的道具和HomeComponent被正确地权HOC,这将返回一个新的有效成分包裹。

That being said, there's a lot we could do to improve this HoC!话虽如此,我们可以做很多事情来改进这个 HoC!


1. If you're unsure about using the index.js file for a container component, you can refactor this however you like, say a withAuthContainer.jsx file which is either exported in the index or it lets the developer choose the one they need. 1. 如果您不确定是否将index.js文件用于容器组件,您可以根据自己的喜好重构它,比如一个withAuthContainer.jsx文件,它要么在索引中导出,要么让开发人员选择他们需要的文件.

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

By definition, this means that passing parameter as a pure component to AuthenticationHOC will return another component.根据定义,这意味着将参数作为纯组件传递给AuthenticationHOC将返回另一个组件。 But here you are passing another HOC ie connect() which is not a component but a wrapper.但是在这里你传递了另一个 HOC,即connect() ,它不是一个组件,而是一个包装器。 So by definition, return <Component /> which resolves as return <connect(mapStateToProps) /> will yield syntax error or a runtime error.因此,根据定义, return <Component />解析为return <connect(mapStateToProps) />将产生语法错误或运行时错误。

Passing a pure component as some HomeComponent will work as it's only a component.将纯组件作为某些HomeComponent传递会起作用,因为它只是一个组件。

My guess is that behind the scene, connect() does currying .我的猜测是在幕后, connect()确实currying What it does is returns a component wrapper with its mapStateToProps and mapDispatchToProps as additional props injected in. Source - https://react-redux.js.org/api/connect#connect-returns它所做的是返回一个组件包装器,其中mapStateToPropsmapDispatchToProps作为注入的附加道具。来源 - https://react-redux.js.org/api/connect#connect-returns

As stated in connect() :connect()

The return of connect() is a wrapper function that takes your component and returns a wrapper component with the additional props it injects. connect() 的返回是一个包装器函数,它接受您的组件并返回一个带有它注入的附加道具的包装器组件。

Thus, we can invert the sequence to:因此,我们可以将序列反转为:

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

and make sure to pass props in your HOC并确保在你的 HOC 中传递props

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM