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?
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.
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);
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With