Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active July 17, 2020 16:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OliverJAsh/036c88f9195c0fa4847b0acd65b2131f to your computer and use it in GitHub Desktop.
Save OliverJAsh/036c88f9195c0fa4847b0acd65b2131f to your computer and use it in GitHub Desktop.
My frustrations writing demos/tests for redux-react connected components

My frustrations writing demos/tests for redux-react connected components

I'm writing demos for my React components, to showcase them in a specific states (e.g. Storybook).

However, the components I'm trying to write demos for are React Redux connected components, and this is making the demos much more difficult to achieve. To illustrate why, I'll start with an example.

Note: the problems I describe here also apply to writing tests (which demos are just one form of).

Example

Imagine we have a Photo component, which receives a size prop of type "small" | "large".

Photo is a connected component, which maps isLoggedIn Redux state to the size prop.

const mapStateToProps = state => ({ size: state.isLoggedIn ? "large" : "small" });

Now imagine I want to write demos for Photo, showcasing the component when its size prop is set to "large".

Using the connected component

We could write this demo against the connected component by mocking the Redux store.

// Demo: Photo: large

const initialState = {
  // Needed to force `Photo`'s `size` prop to `"large"`
  isLoggedIn: true,
};
const store = createStore(reducer, initialState);

const demo = (
  <Provider store={store}>
    <Photo />
  </Provider>
);

However, this is not ideal. Instead of just passing in props like size="large", we have to define the Redux state necessary to derive that prop. But in the context of our demo, this context (isLoggedIn: true) is seemingly entirely unrelated to behaviour our demo wants to simulate (size="large"). Furthermore, the logic our component uses to derive that prop from Redux state could change at any point, and when that happens the demos will silently break. Not good!

Using the unconnected component

Alternatively, we could write this demo against the unconnected component.

// Demo: Photo: large

const demo = <Photo size="large" />;

However this approach falls down when a child component is connected.

For example, imagine our Photo component uses another component called Overlay inside. The Overlay receives a darkMode prop of type boolean.

Overlay is a connected component, which maps isNewUser Redux state to the darkMode prop.

const mapStateToProps = state => ({ darkMode: state.isNewUser });

Now imagine we want to write demos for Photo, showcasing the component when its size prop is set to "large" and when the child component Overlay's darkMode prop is set to true.

Using the unconnected Photo, we can easily pass in our desired size prop. But we can't pass in our desired darkMode prop to Overlay. Because this child component is connected, we'll have to resort to mocking the store once again. Unfortunately, this takes us back to the problems I mentioned previously.

// Demo: Photo: large and dark overlay

const initialState = {
  // Needed to force `Overlay`'s `darkMode` prop to `true`
  isNewUser: true,
};
const store = createStore(reducer, initialState);

const demo = (
  <Provider store={store}>
    <Photo size="large" />
  </Provider>
);

We wouldn't have this problem if none of the child components were connected. Unfortunately this is at odds with the established best practices for performance. In React Redux, the best practice is to pass IDs down to children and connect at each level of the tree. This way, parent components won't need to re-render if the state needed by a child component changes. (More information in this article.)

Other solutions?

Are there any other, better ways of writing a demo for Photo? The main thing I want to avoid (for the reasons mentioned previously) is using the connected component and having to mock Redux state in order to simulate different behaviours—I would much prefer to explicitly pass props.

Ideally, in our demo, Photo's usage would look like this:

// Demo: Photo: large and dark overlay

const demo = <Photo size="large" overlayProps={{ darkMode: true }} />;

… and in our production app, Photo's usage would look like this:

// Fully connected
<Photo />
@einarq
Copy link

einarq commented Jun 30, 2019

I guess there are many different solutions to this. Personally I tend to either test the connected component with a mocked store (which just has the initialState of every reducer), or use shallow rendering to test the inner components.
All state should be derived using selectors, and those are tested separately.

@OliverJAsh
Copy link
Author

OliverJAsh commented Jun 30, 2019

@einarq

Re. using the connected components, I outlined some problems regarding that, which I see as quite significant, so unfortunately that's off the table.

Shallow rendering would avoid the problem where an unconnected component has connected children, but unfortunately shallow rendering isn't an option for component demos, because we want to demo the whole thing, not just the container.

@einarq
Copy link

einarq commented Jun 30, 2019 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment