April 29, 2020

The Connected Component Pattern

A fancy way of separating your presentation layer from your data layer in React.

unsplash-logoTyler Lastovich

We love React because it makes it easy for us to write a lot of components, quickly. The only difficult part is managing the inherent complexity that comes with all those components.

I've come to find that when testing, as expected the more complex your component is the more time it will take to test it thoroughly. This post will provide a small pattern to help reduce that complexity when working with data from a remote source.


When I first started working on my current project, we were heavily using the typical React Redux connect()() pattern. We would define our components, then use the connect()() higher order function to connect it to our redux store. It would normally look like this:

export const SuperCoolComponent = ({ data, loading, error, fetchData }) => {
// dispatch fetch data on component mount
useEffect(() => {
fetchData();
}, [])
if (loading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorPresentation error={error} />;
}
return (
<div>
{...displayDataHere}
</div>
);
};
// grabbing redux actions for use in this component
const mapDispatchToProps = (dispatch) => bindActionCreators({
fetchData,
}, dispatch);
// getting state of data
const mapStateToProps = (state) => {
return { ...state.superCoolComponent };
};
export default connect(mapStateToProps, mapDispatchToProps)(SuperCoolComponent);

This worked great for us at the time. Testing was extremely easy. When testing just the component and it's behavior, we could test the non-connected component and simulate the props with ease:

import ConnectedSuperCoolComponent, { SuperCoolComponent } from './SuperCoolComponent';
test('component renders successfully', () => {
const props = {
data: {
someCoolData: 'is here',
},
loading: false,
error: null,
};
const c = render(<SuperCoolComponent {...props} />);
// get actual and expected
expect(actual).toBe(expected);
});

It also makes it extremely easy to test behavior:

import ConnectedSuperCoolComponent, { SuperCoolComponent } from './SuperCoolComponent';
test('component shows loading properly', () => {
const props = {
data: undefined,
loading: true, // <--- SIMPLE SIMULATION 😎
error: null,
};
const c = render(<SuperCoolComponent {...props} />);
// expect loading to be there
expect(actual).toBe(expected);
});

This is awesome because we don't have to mock anything to test some behavior. Then, all you have to worry about is testing your reducers and actions. Then one final test for the ConnectedSuperCoolComponent in which you will have to mock your remote source (whether through axios mocks or whatever).


This worked great for us in our small project. But as expected, the application started to grow. We started to use our Redux store to hold any information that came from a remote source. The store was growing fast with no stop in sight. It was at this point I came to realize, lets not put all of our state globally (and actually have state as low as possible).

This was an important item to learn. I could go on and on about how this is bad and why state should be low, but that isn't the focus of our conversation today. To learn more about state management and where it should be in your tree, I recommend Kent C. Dodd's post Application State Management with React.

So we migrated to using some custom hooks to manage state, some working with useSWR and others just plain reducer pattern. The same component above would be refactored to look like this:

import useSWR from 'swr'
const SuperCoolComponent = () => {
const { data, loading = !data, error } = useSWR('super.cool/url');
if (loading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorPresentation error={error} />;
}
return (
<div>
{...displayDataHere}
</div>
);
};
export default SuperCoolComponent;

This felt great! We got all of the benefits of SWR and components were easy to write. We did not have to write a bunch of reducer / action boilerplate. One line and it was simple to define our components.

We were feeling good. That is, until testing came around.

Now just to test each behavior of the component, we had to mock useSWR. It felt dirty and required a ton of boilerplate for mocking. We traded the complexity in implementation (redux plumbing) with complexity in testing (mocking SWR hooks). The above tests turned out to look like this:

import SuperCoolComponent from './SuperCoolComponent';
jest.mock('swr')
test('component renders successfully', () => {
useSWR.mockImplementationOnce(() => ({
data: {
someCoolData: 'is here',
},
loading: false,
error: null,
})
const c = render(<SuperCoolComponent {...props} />);
// get actual and expected
expect(actual).toBe(expected);
});
test('component shows loading properly', () => {
useSWR.mockImplementationOnce(() => ({
data: undefined,
loading: true,
error: null,
})
const c = render(<SuperCoolComponent {...props} />);
// expect loading to be there
expect(actual).toBe(expected);
});

This doesn't look too bad, but once you start having async tests, jest mocks start get get all mixed up and you do not get the results you expect anymore. Async testing is hard, and we found a solution to this but it just added more boilerplate. It gets even worse when you have to fetch additional data from a context or have multiple remote sources for the component.


This is where the Connected Component Pattern came back. I loved how I could test things separately with the redux connected component, and so I came up with the following pattern in order to do so:

import useSWR from 'swr'
export const SuperCoolComponent = ({ data, loading, error }) => {
if (loading) {
return <LoadingSkeleton />;
}
if (error) {
return <ErrorPresentation error={error} />;
}
return (
<div>
{...displayDataHere}
</div>
);
};
const ConnectedSuperCoolComponent = () => {
const { data, loading = !data, error } = useSWR('super.cool/url');
const props = {
data,
loading,
error,
};
return <SuperCoolComponent {...props} />;
};
export default ConnectedSuperCoolComponent;

This allowed my old tests for only testing component behavior to work without any changes. I only had to change the mocking of the testing of ConnectedSuperCoolComponent. It also maintains the Single Responsibility Principle in that the connected component's responsibility is fetching data and the actual component only worries about presentation.

I have been using this pattern throughout the application now and love it. Let me know what you think! Tweet me @bstncartwright 😃.

Special thanks to Adam Whitehurst for helping prepare this post. 🤘

Join the Newsletter

I'm putting a focus into creating rich tech content this year. Readers will receive a weekly email full of this content and some other cool stuff I find throughout the week. There will be no spam and you can unsubscribe at any time.

© Boston Cartwright 2020