Unit testing connected higher-order components in React/Enzyme - javascript

In normal connected React components, in order to make unit testing easier, I've seen the pattern of exporting the base component, while export defaulting the connected component. It works well for simple tests that handle the actual functionality of the component without getting cluttered with redux stores and router mocking.
I'm looking for an analog for higher-order components. For instance, given a trivial higher-order component:
export default function exampleHoc(Wrapped) {
const ExampleHoc = props => (
<Wrapped {...props.reduxStore} hoc={true} />
);
return connect(
state => ({
reduxStore: state.example.reduxStore,
}))(ExampleHoc);
}
I'd like to verify that props.hoc === true in a unit test, without having to wire up the other higher-order components that <ExampleHoc> strings along.
My best design is passing along a flag:
export default function exampleHoc(Wrapped, exportUnconnectedForTest) {
const ExampleHoc = props => (
<Wrapped {...props.reduxStore} hoc={true} />
);
if (exportUnconnectedForTest) {
return ExampleHoc;
}
return connect(
state => ({
reduxStore: state.example.reduxStore,
}))(ExampleHoc);
}
This makes it easy to test
const DummyComponent = () => <div />;
const Sut = exampleHoc(DummyComponent, true);
...
test('props value should be passed', () => {
const sut = shallow(<Sut />);
const childProps = sut.find(DummyComponent).props();
expect(sut.childProps.hoc).to.equal(true);
});
while still happily passing along the wrapped output to any regular callers of the method.
It works, but it seems clunky to me. Is there a better/established pattern for delivering the unwrapped interior of a higher-order component?

Related

How to unit test a higher order component using jest and enzyme in react

I am currently trying to write a test to test what is inside of a higher order components
my test like so:
let Component = withEverything(Header);
let wrapper;
it('renders correctly', async () => {
wrapper = await mountWithSleep(
<Component componentProps={{ session: { id: '2' } }} />,
0.25
);
console.log(wrapper.debug());
});
});
outputs the following:
<Component>
<WithSession component={[Function: GlobalNav]} innerProps={{...}} />
</Component>
My with session file looks like the following:
import React, { Component, ComponentType } from 'react';
import { Session, session } from '#efa/web/src/modules/auth/authService';
import { Omit } from '#everlutionsk/helpers';
import { Subscription } from 'rxjs';
class WithSession extends Component<Props, State> {
state: State = {
session: undefined
};
private subscription: Subscription;
componentDidMount() {
this.subscription = session.subscribe(session => {
this.setState({ session });
});
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
if (this.state.session === undefined) return null;
const Component = this.props.component;
const props = { ...this.props.innerProps, session: this.state.session };
return <Component {...props} />;
}
}
/**
* Injects a current session to the given [component].
*/
export function withSession<P extends SessionProps>(
component: ComponentType<P>
): ComponentType<Omit<P, keyof SessionProps>> {
return props => <WithSession component={component} innerProps={props} />;
}
export interface SessionProps {
readonly session: Session | null;
}
interface Props {
readonly component: ComponentType;
readonly innerProps: any;
}
interface State {
readonly session: Session | null | undefined;
}
I have tried to do a jest.mock which gets me part of the way using this:
jest.mock('#efa/web/src/modules/auth/components/withSession', () => {
//#ts-ignore
const original = jest.requireActual(
'#efa/web/src/modules/auth/components/withSession'
);
return {
__esModule: true,
...original,
withSession: component => {
return component;
}
};
});
Using this module i can at least see a returned component instead of with session. But now the issue is i need to be able to set the session state. wondering if anyone can help?!
It is worth noting this is a project which we inherited i would not of implemented it this way ever!
You're correct that this type of code absolutely makes testing more difficult. The "best" way in this case its probably a bit of work because the nicest way to go about it would be to switch this thing to context; and then add options to your test framework mount method to populate that context how you want from individual tests. Though, you can reach something similar with old fashioned HOCs.
There's a cheaper way, and that would be to allow options to be passed to withEverything (if this is used by the app as well, you can create a mirror one called withTestHocs or similiar. I.e.
withTestHocs(Header, {
session: //... object here
})
Internally, this HOC would no longer call withSession whatsoever. Instead, it would call a HOC who's only purpose is to inject that session config object into the component for test reasons.
There's no reason to do complex mocking to get the session right on every test, its a waste of time. You only need that if you're actually testing withSession itself. Here you should be prioritising your test framework API that makes having custom session per test nice and simple. jest.mock is not easily parametrised, so that in itself is also another good reason to not go down that road. Again, the exception is when you're unit testing the actual session hoc but those tests are typically quite edge-casey and wont be using the core "test framework HOC" you'll use for all your userland/feature code -- which is what I'm focusing on here.
Note with this solution, you wouldn't need the complex jest mocking anymore (provided all tests were moved to the new way).
export const withEverything =
(Component, {session}) =>
({ providerProps, componentProps }) =>
(
<MockedProvider {...providerProps}>
<BrowserRouter>
<FlashMessage>
<Component {...componentProps} session={session} />
</FlashMessage>
</BrowserRouter>
</MockedProvider>
);
Now in your test
it('renders correctly', async () => {
wrapper = await mountWithSleep(
const withEverything(Header, { session: { id: '2' }}),
0.25
);
console.log(wrapper.debug());
});
If you need to be able to manipulate the session mid-test, you could do that by returning a method from withEverthing that allows the session to be set, but im not sure if you need it.

React HOC: Pass data attributes to the first child/element of wrapped component

I have a hoc component like this:
export const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => (
<WrappedComponent {...props} data-test-id="this-is-a-element" />
);
return ModifiedComponent;
};
export default withAttrs;
and I use it like this:
import React from 'react';
import withAttrs from './withAttrs';
const SomeLink = () => <a><p>hey</p</a>;
export default withAttrs(SomeLink);
I expect to have an anchor tag like this:
<a data-test-id="this-is-a-element"><p>hey</p></a>
But the hoc doesn't add the data-attribute to the first element. Is there a way to achieve this?
But the hoc doesn't add the data-attribute to the first element.
It's not the HOC that isn't adding it, it's SomeLink, which doesn't do anything with the props the HOC passes to it.
The simple answer is to update SomeLink:
const SomeLink = (props) => <a {...props}><p>hey</p></a>;
That's by far the better thing to do than the following.
If you can't do that, you could make your HOC add the property after the fact, but it seems inappropriate to have the HOC reach inside the component and change things. In fact, React makes the element objects it creates immutable, which strongly suggests you shouldn't try to mess with them.
Still, it's possible, it's probably just a bad idea:
export const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => {
// Note we're *calling* the function, not just putting it in
// a React element via JSX; we're using it as a subroutine of
// this component rather than as its own component.
// This will only work with function components. (You could
// write a version that handles class components as well,
// but offhand I don't think you can make one HOC that handles
// both in this case.)
const result = WrappedComponent(props);
return {
...result,
props: {
...result.props,
"data-test-id": "this-is-a-element",
},
};
};
return ModifiedComponent;
};
/*export*/ const withAttrs = (WrappedComponent) => {
const ModifiedComponent = (props) => {
// Note we're *calling* the function, not just putting it in
// a React element via JSX; we're using it as a subroutine of
// this component rather than as its own component.
// This will only work with function components. (You could
// write a version that handles class components as well,
// but offhand I don't think you can make one HOC that handles
// both in this case.)
const result = WrappedComponent(props);
// THIS IS PROBABLY A VERY BAD IDEA. React makes these objects
// immutable, probably for a reason. We shouldn't be mucking
// with them.
return {
...result,
props: {
...result.props,
"data-test-id": "this-is-a-element",
},
};
};
return ModifiedComponent;
};
const SomeLink = () => <a><p>hey</p></a>;
const SomeLinkWrapped = withAttrs(SomeLink);
const Example = () => {
return <div>
<div>Unwrapped:</div>
<SomeLink />
<div>Wrapped:</div>
<SomeLinkWrapped />
</div>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
/* So we can see that it was applied */
[data-test-id=this-is-a-element] {
color: green;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Again, I don't think I'd do that except as a very last resort, and I wouldn't be surprised if it breaks in future versions of React.

React call functions on renderless component

I need to have a component for handling settings, this component (called Settings) stores state using useState(), for example the primary color.
I need to create a single instance of this component and make it available to every component in the app. Luckily, I already pass down a state dict to every component (I'm very unsure if this is the correct way to achieve that btw), so I can just include this Settings constant.
My problem is that I don't know how to create the component for this purpose, so that I can call its functions and pass it to children.
Here is roughly what my Settings component looks like:
const Settings = (props) => {
const [primaryColor, setPrimaryColor] = useState("")
const getColorTheme = (): string => {
return primaryColor
}
const setColorTheme = (color: string): void => {
setPrimaryColor(color)
}
return null
}
export default Settings
Then I would like to be able to do something like this somewhere else in the app:
const App = () => {
const settings = <Settings />
return (
<div style={{ color: settings.getColorTheme() }}></div>
)
}
Bear in mind that I'm completely new to react, so my approach is probably completely wrong.
You can use a custom Higher Order Component(HOC) for this purpose, which is easier than creating a context(even thougn context is also a HOC). A HOC takes a component and returns a new component. You can send any data from your HOC to the received component.
const withSettings = (Component) => {
const [settings, setSettings] = useState({})
// ...
// ...
<Component {...props} settings={settings}/>
);
And you can use it like this:
const Component = ({ settings }) => {
...your settings UI
}
export default SettingsUI = withSettings(Component);
You can read more about HOCs in the official react documentation

How to pass arguments to a function inside compose?

I have a function like so:
export const fireView = (prop1, prop2) => WrappedComponent =>
class extends Component {
//stuff in here
}
then a hoc like this:
export const withFireView = (prop1, prop2) =>
fireView(prop1, prop2)
but now I want to put this function inside compose as I need to call it with mapStateToProps
so I did this:
compose (
connect(mapStateToProps),
fireView
)
but it breaks because You must pass a component to the function returned by connect
so I then I get rid of the arguments in the fireview function and I think this will work but then all the arguments are now undefined inside the function as I've not passed them
Is there any way to do something like this:
compose (
connect(mapStateToProps),
fireView(arg1, arg2)
)
but obviously, they are not defined there if that makes sense.
Here is a full working example:
var Component = React.Component;
var render = ReactDOM.render;
var Provider = ReactRedux.Provider;
var connect = ReactRedux.connect;
var createStore = Redux.createStore;
var compose = Redux.compose;
const reducer = () => {return {}};
const mapStateToProps = () => {
return {};
};
const wrapped = (props) => {
return <div>{props.prop1} {props.prop2}</div>;
}
const fireView = (prop1, prop2) => WrappedComponent => {
return class extends Component {
render() {
return <WrappedComponent prop1={prop1} prop2={prop2} />;
}
}
}
const withFireView = (prop1, prop2) => fireView(prop1, prop2);
const withFireViewAndConnect = (arg1, arg2) => compose(connect(mapStateToProps), withFireView(arg1, arg2));
const App = withFireViewAndConnect('some', 'arg')(wrapped);
render(<Provider store={createStore(reducer)}><App /></Provider>, document.getElementById('demo'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.13.0/polyfill.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.0/react-redux.js"></script>
<div id="demo"></div>
I had a similar requirement and ended up on this page. I supposed we were not able to understand the use case. I will try to summarise it and also provide an answer to it.
Explanation
We can use HOC (Higher Order Components) in React to abstract out common functionality. In React docs, we can see the 'with' notation to use these HOCs. I have created a few on my app as well.
withSplashScreenRedirect, withHistoryObject
All these HOCs that I created takes a Component (WrappedComponent) and returns a Component. (No additional Arguments required).
const EnhancedComponent = higherOrderComponent(WrappedComponent);
I would generally have around 2 to 3 HOCs on each Screen level Component. Writing these HOCs as functions returning other functions look ugly. Redux provides a useful compose function. Compose helps us to chain these HOCs in a more readable manner.
From compose docs.
All compose does is let you write deeply nested function transformations without the rightward drift of the code. Don't give it too much credit!
Soon I wanted to create another HOC that would standardise my API call logic and API call handling logic. In this case, I had to send two additional functions to my HOC (along with the Wrapped Component).
withApiCallHandler(WrappedComponent, makeApiCall, handleApiResponse)
Passing props to HOC is not a big deal. React Docs have a good example explaining how to do it. However, adding such an HOC to compose chain is not so straightforward. I guess that is what OP was trying to ask here.
Answer
I wasn't able to find a way to chain such a HOC with compose. Maybe it's not possible. The best solution would be to initialise the withApiCallHandler Component separately and then chain it with compose.
Instead of
const ComposedScreenComponent = compose(
withHOC1,
withHOC2,
withHOC3(arg1, arg2)
)(ScreenComponent);
Do this
const withHOC3AndArgs = (WrappedComponent) =>
withHOC3(WrappedComponent, arg1, arg2);
const ComposedScreenComponent = compose(
withHOC1,
withHOC2,
withHOC3AndArgs(arg1, arg2)
)(ScreenComponent);
This is not the best answer to the question, but if someone is stuck then this solution will surely be a good workaround.
You can make withFireView return a function instead. All will work.
export const withFireView = (prop1, prop2) => () => fireView(prop1, prop2)

React Redux -- can I make mapStateToProps only take in part of the state?

I want to make reusable modules that could be plugged in to any react-redux application. Ideally, my module would have a container component, actions, and reducer at the top level (and then any presentational components below the container). I would want the module to only work off its own slice of the app's state, and ideally to not have to know anything about the rest of the app state (so it's truly modular).
Reducers only work off of part of the state (using combineReducers), so I'm happy there. However, with container components, it seems like mapStateToProps always takes in the full state of the app.
I'd like it if mapStateToProps only took in the same "state slice" that I am handling in my module (like the reducer does). That way my module would truly be modular. Is this possible? I guess I could just pass that slice of the state down to be the props of this component (so I could just use the second argument of mapStateToProps, ownProps), but am not sure if this would have the same effect.
That is actually something of a complicated topic. Because Redux is a single global store, the idea of a completely encapsulated, fully reusable plug-and-play set of logic does become rather difficult. In particular, while the reducer logic can be fairly generic and ignorant of where it lives, the selector functions need to know where in the tree to find that data.
The specific answer to your question is "no, mapState is always given the complete state tree".
I do have links to a number of relevant resources, which may possibly help with your situation:
There's several existing libraries that try to implement "per-component state in Redux". I have a list of them in my Redux addons catalog, in the Component State category.
A group of devs have been discussing and prototyping various approaches to the "reusable logic module in Redux" concept. Their work is at https://github.com/slorber/scalable-frontend-with-elm-or-redux .
Randy Coulman recently posted a three-part blog series related to state encapsulation and modularity in Redux. He didn't come up with definitive answers, but the posts are worth reading: Encapsulating the Redux State Tree, Redux Reducer Asymmetry, and Modular Reducers and Selectors.
Although mapStateToProps (the first function you pass to connect) gets passed the whole store as you said, its job is to map specific parts of the state to the component. So only what is returned from mapStateToProps will be mapped as a prop to your component.
So lets say your state looks like this:
{
account: {
username: "Jane Doe",
email: "janedoe#somemail.com",
password: "12345",
....
},
someOtherStuff: {
foo: 'bar',
foo2: 'bar2'
},
yetMoreStuff: {
usuless: true,
notNeeded: true
}
}
and your component needs everything from account and foo from someOtherStuff then your mapStateToProps would look like this:
const mapStateToProps = ({ account, someOtherStuff }) => ({
account,
foo: { someOtherStuff }
});
export default connect(mapStateToProps)(ComponentName)
then your component will have the prop account and foo mapped from your redux state.
Redux only has a single store as you know, so all it knows to do is pass the entire store to your mapStateToProps function. However using object destructuring, you can specify which properties in the store you want and ignore the rest. Something like 'function mapStateToProps({prop1, prop2})' would only capture those two properties in the store and ignore the rest. Your function is still receiving the entire store, but you're indicating that only these props interest you.
In my example, 'prop1' and 'prop2' would be the names you assigned your reducers during the call to 'combineReducers'.
Ideally the way it works is you get the state and you extract the values from them by use deconstructors. redux works on concept of single state
For example:-
function mapStateToProps(state){
const { auth } = state //just taking a auth as example.
return{
auth
}
}
I'm running into the same problem because, as you said, the current implementation of redux/react-redux allows for splitting up reducers on the state just fine but mapDispatchToProps always passes the whole state tree.
https://stackoverflow.com/a/39757853/444794 is not what I want, because it means we have to duplicate all our selector logic across each react-redux application that uses our module.
My current workaround has been to pass the slice of the state down as a prop instead. This follows a sort of compositional pattern but at the same time removes the cleanliness of accessing the state directly, which I'm disappointed with.
Example:
Generally, you want to do this:
const mapStateToProps = (state) => {
return {
items: mySelector(state)
}
}
const mapDispatchToProps = (dispatch) => {
return {
doStuff: (item) => {
dispatch(doStuff(item))
}
}
}
class ModularComponent extends React.Component {
render() {
return (
<div>
{ this.props.items.map((item) => {
<h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
})}
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)
but since this module is included in an application where the state is now several things (ie. key-values) rather than a list of items, this won't work. My workaround instead looks like:
const mapStateToProps = (_, ownProps) => {
return {
items: mySelector(ownProps.items)
}
}
const mapDispatchToProps = (dispatch) => {
return {
doStuff: (item) => {
dispatch(doStuff(item))
}
}
}
class ModularComponent extends React.Component {
render() {
return (
<div>
{ this.props.items.map((item) => {
<h1 onclick={ () => this.props.doStuff(item) }>{item.title}</h1>
})}
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ModularComponent)
And the application using the Module looks like:
const mapStateToProps = (state) => {
return {
items: state.items
stuffForAnotherModule: state.otherStuff
}
}
class Application extends React.Component {
render() {
return (
<div>
<ModularComponent items={ this.props.items } />
<OtherComponent stuff={ this.props.stuffForAnotherModule } />
</div>
)
}
}
export default connect(mapStateToProps)(Application)
You do have the option of writing a couple of wrapper utils for your modules that will do the work of: 1) Only running mapStateToProps when the module's slice of state changes and 2) only passes in the module's slice into mapStateToProps.
This all assumes your module slices of state are root properties on the app state object (e.g. state.module1, state.module2).
Custom areStatesEqual wrapper function that ensures mapStateToProps will only run if the module's sub-state changes:
function areSubstatesEqual(substateName) {
return function areSubstatesEqual(next, prev) {
return next[substateName] === prev[substateName];
};
}
Then pass it into connect:
connect(mapStateToProps, mapConnectToProps, null, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
Custom mapStateToProps wrapper that only passes in the module substate:
function mapSubstateToProps(substateName, mapStateToProps) {
var numArgs = mapStateToProps.length;
if (numArgs !== 1) {
return function(state, ownProps) {
return mapStateToProps(state[substateName], ownProps);
};
}
return function(state) {
return mapStateToProps(state[substateName]);
};
}
And you'd use it like so:
function myComponentMapStateToProps(state) {
// Transform state
return props;
}
var mapSubstate = mapSubstateToProps('myModuleSubstateName', myComponentMapStateToProps);
connect(mapSubstate, mapDispatchToState, null, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
})(MyModuleComponent);
While untested, that last example should only run myComponentMapStateToProps when 'myModuleSubstateName' state changes, and it will only receive the module substate.
One additional enhancement could be to write your own module-based connect function that takes one additional moduleName param:
function moduleConnect(moduleName, mapStateToProps, mapDispatchToProps, mergeProps, options) {
var _mapState = mapSubstateToProps(moduleName, mapStateToProps);
var _options = Object.assign({}, options, {
areStatesEqual: areSubstatesEqual('myModuleSubstateName')
});
return connect(_mapState, mapDispatchToProps, mergeProps, _options);
}
Then each module component would just need to do:
moduleConnect('myModuleName', myMapStateToProps)(MyModuleComponent);
The answer to your question is yes. Both given answers cover different aspects of the same thing. First, Redux creates a single store with multiple reducers. So you'll want to combine them like so:
export default combineReducers({
people: peopleReducer,
departments: departmentsReducer,
auth: authenticationReducer
});
Then, say you have a DepartmentsList component, you may just need to map the departments from the store to your component (and maybe some actions mapped to props as well):
function mapStateToProps(state) {
return { departments: state.departments.departmentsList };
}
export default connect(mapStateToProps, { fetchDepartments: fetchDepartments })(DepartmentsListComponent);
Then inside your component it is basically:
this.props.departments
this.props.fetchDepartments()

Categories

Resources