Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. If not, then dispatch an action to try and load the token from the 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

then I did a simplified version

I replaced the code from my HOC to the most simple version

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! ...works now!

So I suspect that connect returns an object that can't be used as a HoC function. Is this correct? What could I do here?

like image 310
clagccs Avatar asked Dec 08 '22 11:12

clagccs


1 Answers

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.

All compose does is let you write deeply nested function transformations without the rightward drift of the code.

So here, we can use it to nest HoCs but in a readable way.

// 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.

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

const withAuth = (WrappedComponent) => connect(mapState)(
  AuthenticationHOC(WrappedComponent)
);

export default withAuth;

Then, you can use your auth HoC transparently.

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

Writing a clean and testable 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.

- 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/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;

See? Our HoC is now unaware of the store and routing logic. 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.

Then, we only provide the props in the index.js, like a container component.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 about the onUnauthenticated as a store action is that different reducers could now react to it, like wiping the user data, reset the dashboard data, etc.

Testing the HoC

Then, it's possible to test the isolated logic of the withAuth HoC with something like Jest and enzyme.

// 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.

react-redux's 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)

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. 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.

That being said, there's a lot we could do to improve this 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.

like image 90
Emile Bergeron Avatar answered Dec 11 '22 09:12

Emile Bergeron