Here at Mews, we have, as any other diligent react/redux developers, followed the redux documentation's recommendations on shaping the redux store. Thus, our state ended up being (somewhat) shaped in such a manner that it references data from one (sub)state to another.

While the documentation has good reasoning behind this structure, we were left to our own devices as to how to properly retrieve cross-referenced data from the store.

If you’ve followed the same principles and were also left scratching your head and contemplating the same problem, you’ve come to the right article.

Caught in the tangle πŸ”€

Let’s take the example presented to us in the documentation. For the sake of consistency and clarity, we’ll change the keys author and username to authorId and id respectively:

{
    posts: {
        byId : {
            post1: {
                id: "post1",
                authorId: "user1",
                body: "......",
                comments: ["comment1", ...]
            },
            ...
        },
        allIds: ["post1", ...]
    },
    users: {
        byId: {
            user1: {
                id: "user1"
                name: "User 1",
            },
            ...
        },
        allIds: ["user1", ...]
    },
    comments: {
        byId: {
            comment1: {
                id: "comment1",
                authorId: "user2",
                comment: ".....",
            },
            ...
        },
        allIds: ["comment1", ...]
    },
    ...
}

Imagine being given that state with the task of creating a component to render a post and its associated data. To start out, the router will provide our component with an ID of the post which we can pass as props to selectors.

Our naive approach πŸ’©

Retrieving the body of the post is pretty straightforward. By making use of a redux binder (e.g. connect from react-redux), our component connects to redux state, where we can pass the ID of the post as prop to a selector in mapStateToProps function.

const mapStateToProps = (state, ownProps) => ({
    // ownProps: { id: <current post ID>, ... }
    body: getPostBody(state, ownProps),
    ...,
})
      
export default connect(
    mapStateToProps,
)(PostDetail);

Within the selector, we can use the ID to access its associated post object, and by extension its body, from the state. For that matter, and throughout this article, we'll use createSelector from reselect for creating selectors. What you need to know about createSelector is that its first arguments are selectors whose result are then passed (in order) to a combiner function which is the last argument of createSelector.

const getPost = (state, props) => state.posts.byId[props.id];
const getPostBody = createSelector(getPost, post => post.body);

That's the easy part done. The tricky part comes with accessing the author's data using the post's ID. Let's say we want to show the name of a post’s author in our component. To achieve this, we will need to pull the application's state object into our combiner and call a user selector at that place:

const getApplicationState = state => state;

const getPostAuthorId = createSelector(getPost, post => post.authorId);

const getUser = (state, props) => state.users.byId[props.id];
const getUserName = createSelector(getUser, user => user.name);

const getAuthorName = createSelector(
    getState,
    getPostAuthorId,
    (state, authorId) => getUserName(state, { id: authorId })
);

This was one of the ways we dealt with tasks concerning cross-referencing states. Granted, it does look smelly with the getApplicationState selector and with the usage of a getUserName inside the combiner, but, well, our naive construct does its job just fine... That is, until you need to get more data out of the user.

See, the problem is that we will have to repeat this pattern throughout the app as soon as we need to pass authorId from the combiner arguments to other 'intermediary selectors.’ Coming back to our task, we want to render a user image in our component. The selector for retrieving the image URL would look very similar:

/*
user:
{
    id,
    name,
    imageUrl,
}
*/

const getUserImage = createSelector(getUser, user => user.imageUrl);

const getAuthorImage = createSelector(
    getState,
    getPostAuthorId,
    // the only differentiating code is the selector used in combiner
    (state, authorId) => getUserImage(state, { id: authorId })
)

As developers, we naturally tend to shun code duplication. This got us thinking. There must be a way to avoid this duplication... right?

Access from the wrong neighborhood scope 🚫

One might propose a solution where, instead of getting specific parts of the user data, we retrieve the whole object and deconstruct it into the necessary parts.

const getAuthor = createSelector(
    getApplicationState,
    getPostAuthorId,
    (state, author) => getUser(state, { id: authorId })
);

const getAuthorName = createSelector(getAuthor, author => author.name);
const getAuthorImageUrl = createSelector(getAuthor, author => author.imageUrl);

As well-intended as this proposition might be, it merely moves the duplication issue to another context.

Structurally, we want our selectors to be as close to its domain as possible. Yet, in our case above, we extract name and imageUrl within the scope of a post. This becomes a problem when we have multiple selectors like these in our application. Imagine that we want to implement a comments section that retrieves author/user from the same state:

const getComment = (state, props) => state.comments.byId[props.id];

const getComment = createSelector(
    getApplicationState,
    getComment,
    (state, post) => getUser(state, { id: post.author })
);

const getCommenterName = createSelector(getAuthor, author => author.name);
const getCommenterImageUrl = createSelector(getAuthor, author => author.imageUrl);

What we achieved is basically just jumping from one duplication issue to one that’s arguably even worse than before. For instance, now when we decide to change the property names of the user object (e.g., from name to userName or from imageUrl to avatarUrl), we will have to find all the usages of getUser, look at its derived selectors, and change the property names one by one. If you have a big codebase with this anti-pattern scattered all around, that will be an anxiety-inducing task.

This would not have been a problem if our selector for getting user data was only dealing in terms of its own domain, as was the case with getUserName and getUserImage.

So, we want to be able to keep our selectors tightly packed within a domain while avoiding code duplication when dealing with cross-referencing state. How do we achieve this blissful state?

Happiness is only a pair of shoes functions away πŸ‘ πŸ‘ 

When inspecting the 'smelly' example, we can distill the pattern into two simple steps:

const getAuthorUsername = createSelector(
    getState,
    getPostAuthorId,
    (state, authorId) => {
        // 1. creating props from the return value of a selector
        const intermediaryProps = { id: authorId };
        // 2. passing selector enhanced props to a selector
        return getUserName(state, intermediaryProps);
    }
);

Following this insight, let's extract the props creation and props passing process and encapsulate them into reusable, selector-agnostic functions outside createSelector.

Enter the extender πŸ“

What we want to achieve first is to create a props object that is extended with the return value of a selector. Going back to our examples with the post's author, we want to be able to create a function that takes getPostAuthorId and spits out { id: 'someAuthorId' }. This can simply be done like with a selector helper:

const createPropsExtender = (keyName, selector) => (state, props) => ({
    ...props,
    [name]: selector(state, props),
})

const postAuthorIdPropsExtender = createPropsExtender('id', getPostAuthorId);

Calling postAuthorIdPropsExtender selector with the appropriate state and props will return ({ ..., id: 'someAuthorId' }). Note that, unlike in our naive examples, the extended props object additionally contains the props that were initially passed to the selector.

A great benefit of having this props extender is that it can be used in multiple places. Remember our example with the user image? Instead of rewriting the same 'smelly' pattern again, we can call postAuthorIdPropsExtender to get our props object ready for getUserImage.

Snowball of props β›„

After this, we need to pass it to the selector that needs the extended props. In the most general way, this can be achieved with the following helper function:

const composeProps = (...selectors) => (state, props) => selectors.reduceRight((selector, composedProps) => selector(state, composedProps), props)

With this many arrows in one line, the implementation of composeProps warrants some explanations: Our first argument is (...selectors). Essentially we are passing multiple selectors as multiple arguments which will be used as an array of selectors:

(selector1, selector2, selector3, ...) -> [selector1, selector2, selector3, ...]

We apply the reduceRight function to these selectors with props as our initial accumulator value. reduceRight is similar to reduce but it starts the iteration from the end of a function. This is to mimic right-to-left function function compositions like compose in ramda or lodash/fp libraries.
Now, for each selector in the array, we call the current selector with (state, composedProps) where composedProps is the return value of the previous iteration. Therefore, each iteration is calling a selector with the previous props that returns yet another props object.

This iteration pattern with reduce is useful if you need multiple props extenders (i.e., multiple intermediary values for a final selector). However, for simple purposes with just one props extender, this can be simplified to:

const composeProps = (selector2, selector1) => (state, props) => {
    const composedProps = selector1(state, props);
    return selector2(state, composedProps);
}

As you can see, this looks very similar to the combiner function in the naive examples. The crucial part to note, however, is that this is extracted into a helper function and therefore we have this pattern available at any time to apply to our selectors. Furthermore, we don't need to use the getApplicationState selector anymore, since the state is already taken directly from the arguments.

Using composeProps together with createPropsExtender, our final getAuthorName selector will look like this:

const getAuthorName = composeProps(
    getUserName,
    postAuthorIdPropsExtender
);

A map of selectors 🌐

That's already pretty neat, but the use of this pair doesn’t stop there. We can further leverage their reusability by using a mapping function to create selectors bound to a specific extender. Let's take postAuthorIdPropsExtender for example. For our post component, we might need selectors get the author's name or image. In other words, we need to call getAuthorName or getAuthorImageUrl.
What do they all need in common? The post's author ID, which is nicely encapsulated in postAuthorIdPropsExtender. Thus, their selector implementation looks like this:

const getAuthorName = composeProps(getUserName, postAuthorIdPropsExtender);
const getAuthorImageUrl = composeProps(getUserImageUrl, postAuthorIdPropsExtender);

At this point, your senses must be tingling: this is yet another duplication. We can avoid this with the use of the map function to create these selectors. Let's pass the final selectors as arrays and bind them to props composition at each iteration:

const selectors = [getUserName, getUserImageUrl];

export const [
    getAuthorName,
    getAuthorImageUrl
] = selectors.map(selector => composeProps(selector, postAuthorIdPropsExtender));

Since the only variable of the mapping function is postAuthorIdPropsExtender we can further create a general mapping function for binding selectors to the props composition:

const composeWithPropsExtender = propsExtender => selector => composeProps(selector, propsExtender)

const selectors = [getUserName, getUserImageUrl];

export const [
    getAuthorName,
    getAuthorImageUrl
] = selectors.map(composeWithPropsExtender(postAuthorIdPropsExtender));

Riding the Typescript wave 🌊

While we initially started using createPropsExtender and composeProps in Javascript, the Hypescript wave has since crashed into our office. As a consequence we have rewritten our helper functions into TS. Note that type Selector<S, P, R> = (state: S, props: P) => R.

createPropsExtender

type ExtendedProp<K extends string | number | symbol, V> = { [key in K]: V; };

const createPropsExtender = <S, P, R, E>(name: keyof E, selector: Selector<S, P, R>) => (state: S, props: P) => ({
    ...props,
    [name]: selector(state, props),
}) as P & ExtendedProp<typeof name, R>;

export default createPropsExtender;

The one thing that is out of the ordinary is the generic E. It exists to provide name with a stronger type than just string. For instance, if name is 'id', then E is { id: unknown } and since name is of type keyof E, typeof name can only be 'id' with no other unions since E has only one key.

composeProps

type ComposeProps = <S, P1, P2, R>(selector2: Selector<S, P2, R>, selector1: Selector<S, P1, P2>) => Selector<S, P1, R>;

const composeProps: ComposeProps = (selector2, selector1) => (state, props) => {
    const composedProps = selector1(state, props);
    return selector2(state, composedProps);
};

export default composeProps;

Originally, our composeProps was written with the reduce pattern but we quickly hit a wall trying to type its function signature. The biggest hurdle to overcome was finding a way to type the (...selectors) part for an indefinite amount selectors. In the end, we couldn’t find a way but, fortunately, we rarely had situations that needed more selectors being passed to composeProps so we ended up with using the simple case which was much easier to type. Perhaps in version 4.0 with new ways to type tuples we will revisit the reduce pattern.

Clean, simple and reusable β™»

Hopefully this article gave you an insight on how to deal with cross-referencing data in redux state the reusable way. The pattern we have worked through can be applied any time we need values from selectors being passed as props to another selector. If you find any such situation in your codebase, don't be afraid to createPropsExtender and composeProps, you might be surprised how much of it you can reuse in different parts of the code.


Watch the recording of my talk at Frontendisti stream:


For more engineering insights shared by Mews tech team:

You've successfully subscribed to Mews Developers
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Success! Your account is fully activated, you now have access to all content.