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).
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"
.
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!
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.)
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 />
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.