Chapter 11. Structuring Redux code

This chapter covers

  • Understanding popular options for organizing Redux code
  • Scaling the structure of a Redux application

If you remember one thing from this chapter, let it be this: Redux couldn’t care less where you put its constituent parts. The package has a curious dynamic; Redux comes with a tiny API for storing and updating state, but it introduces a comprehensive design pattern into an application. Looking at the Redux architecture diagram, there are few methods provided by Redux to connect all the pieces. Figure 11.1 highlights the location of those methods. As you know, React Redux provides an important role in exposing some of that functionality to the web application.

Figure 11.1. With help from React Redux, Redux exposes a few methods for state management.

The takeaway here is that Redux doesn’t have any reason or ability to dictate how your code is structured. If an action object makes it back to the store, a Redux workflow is satisfied. Whatever mousetrap you build to facilitate this is completely up to you.

The documentation and official examples offer one simple option, but this chapter will explore the most popular code structure alternatives. We won’t say there are no wrong answers for how you should organize your code, but there could be multiple right answers. Ultimately, the right decision for your projects depends on what you’re building. A prototype, a small mobile app, and a large-scale web app each have distinct needs and probably deserve unique project layouts.

11.1. Rails-style pattern

The default organization found in the documentation, example projects, and many tutorials (including this book) is sometimes referred to as Rails-style. Rails, a popular web framework for the Ruby programming language, has a well-defined directory layout in which each file type is grouped within a corresponding directory. For example, you’ll find every model within the model directory, each controller within the controller directory, and so on.

You’ll recall that there are no models or controllers written in Parsnip, but each file type similarly resides in an appropriately named directory: actions, reducers, components, containers, and sagas. Figure 11.2 illustrates this structure, as it’s found in the Parsnip application.

Figure 11.2. A Rails-style organization pattern

This is the default strategy, because of its simplicity. No explanation is required when a new Redux-familiar developer joins the team; the actions are in the actions directory.

11.1.1. Pros

The strengths of the Rails-style approach are clear enough. First, it’s the simplest strategy to teach. If it’s your job to ramp up a developer who’s joined your team, you explain that a container component can be found in the containers directory. The container dispatches an action, which can be found in the actions directory, and so on. The simplicity reduces the onboarding time for new developers.

The second strength of choosing this pattern is its familiarity. Any Redux developer brought onto the team likely has learned Redux using the same pattern. Again, this boils down to minimal onboarding time for new developers. The Rails-style pattern is a great choice for prototypes, hackathon projects, training initiatives, and any application of a similar scope.

A final thought that will make more sense as the chapter progresses: actions and reducers have a many-to-many relationship. It’s common practice for an action to be handled by multiple reducers. The Rails-style organization pattern arguably does the best job of representing that relationship, by keeping actions and reducers in separate peer directories.

11.1.2. Cons

The cost of this approach is scalability. On the extreme end of examples, Facebook consists of more than 30,000 components, as of April 2017. Can you image having all those components within a components or containers directory? To be fair, it would be difficult to find anything in an application that large without Facebook’s serious investment in tooling, regardless of how thoughtful the architecture is.

The fact remains, you won’t need to come anywhere close to Facebook’s scale to experience growing pains with this pattern. In our experience, the Rails-style approach pushes its limits around the 50–100 component range, but every team will find its own breaking point. At the heart of this complaint is how frequently you’ll end up in half a dozen different directories when adding one small feature.

From the directory structure, it’s nearly impossible to tell which files interact with one another. The next pattern alternative attempts to address this concern.

11.2. Domain-style pattern

You’ll find many variations of domain-style, or feature-based, organizations. Generally, the goal of each of these variations is to group related code together. Let’s say your application has a subscription workflow to sign up new customers. Instead of spreading related components, actions, and reducers across several directories, the domain-based pattern advocates for putting them all in one subscription directory, for example.

One possible implementation of this pattern is to loosely label each container component as a feature. See figure 11.3 for an example using Parsnip again. Within the containers directory, you might find a list of feature directories, such as task. Each directory may contain the component, actions, reducers, and more related to the feature.

Figure 11.3. The domain-style, or feature-based, pattern

This pattern works great for some applications. It certainly scales better, because each new feature produces one new directory. The problem, however, is that actions and state are commonly shared across domains. In the case of an e-commerce application, products, the shopping cart, and the rest of the checkout workflow are unique features, but they are heavily dependent on one another’s state.

It’s not unusual that a single container component might pull state from several of these domains to render a page in the checkout process. If, for example, the user adds an item to their shopping cart, you may see updates in the product page, the header, and a shopping cart sidebar. Is there something you can do to better organize shared data?

One alternative domain-style pattern is to leave containers and components in the Rails-style pattern, but organize actions and reducers by domain. Figure 11.4 refactors the last example to demonstrate this pattern. With this alteration, containers no longer live within one specific domain, while they import state from several others.

Figure 11.4. A domain-style alternative pattern that excludes containers

If you prefer, the domain-specific files may live within a data directory instead of within store. Several projects you’ll work on will have minor variations of this nature. These tweaks are the natural result of teams of developers reaching consensus on what makes the most sense for their project (or trying something and running with it.)

11.2.1. Pros

The biggest win is the ability to find more of what you need in one place. If you’ve picked up a ticket to add a new feature to a Header component, you may expect to find everything you need within a Header directory, for example. Whenever you need to add a new feature, you need add only one directory. If you choose to separate the components and containers from your domain directories, that decoupling serves as a good reminder to keep those components reusable.

One nice aspect of maintaining domains within a store directory is that the file structure reflects the top-level keys in your store. When specifying the mapStateToProps function within container components, the slice of Redux store you specify will mirror your file structure.

11.2.2. Cons

You’ll find there are still many tradeoffs to this approach, and you should understand that there’s not a single answer for every application. One of the potential weaknesses in this pattern is the lack of certainty or clarity around what constitutes a domain or feature. There will be times when you’ll debate whether some new functionality belongs in an existing directory or merits the creation of a new one. Adding more decisions and overhead like this to a developer’s work isn’t ideal.

With a domain-style pattern, each project tends to take on at least a few nuances of its own. Introducing new developers to the project will require passing on the tribal knowledge of how your team likes to introduce new features and where lines have been drawn already. Consider that there’s at least a small amount of overhead here.

One concern that isn’t addressed by any discussed variation of this pattern is the idea that actions may be handled by multiple reducers. One domain directory generally contains a single actions file and a single reducer file. Watching for actions in a different domain’s reducer can be a source of confusion. That comes with the territory and should be considered when weighing the tradeoffs in your application. One option you have is to create a domain directory for shared actions.

11.3. Ducks pattern

Before you ask, the name “ducks” is a riff on the last syllable of Redux. The intent of the ducks pattern is to take domain-style organization a step further. The pattern’s author, Erik Rasmussen, observed that the same groupings of imports repeatedly appeared across an application. Instead of keeping a feature’s constants, actions, and reducers in one directory, why not keep them all in a single file?

Does keeping all those entities in one file leave a bad taste in your mouth? The first question many developers ask is whether the file bloat is manageable. Fortunately, you shave off a little boilerplate code with this pattern. The first to go are the import statements required across the action and reducer files. In general, though, it’s helpful to keep each slice of your store fairly flat. If you notice that a ducks file is getting especially large, take the hint and ask yourself if the file can be broken into two feature files.

Going back to Erik’s original proposal, there are a few rules to follow when using this pattern. A module

  • Must have the reducer be the default export
  • Must export its action creators as functions
  • Must have action types in the form npm-module-or-app/reducer/ACTION_TYPE
  • May export its action types as UPPER_SNAKE_CASE

When keeping all these elements in one file, it’s important to have consistency in how you export the various pieces. This rule set was designed for that purpose. The default export of a ducks module should always be the reducer. This frees up the ability to import all the action creators for use in containers with the syntax import * as task-Actions from './modules/tasks', for example.

What does a ducks file look like? Listing 11.1 is an abbreviated example based on Parsnip. You’ll see all your old friends in one place: constants, action creators, and a reducer. Because they’re used only locally, the action type constants can be shortened to remove redundant context. You’ll notice that covering the standard CRUD operations for a resource can be done in a modest number of lines of code.

Listing 11.1. An example ducks file
const FETCH_STARTED = 'parsnip/tasks/FETCH_STARTED';               1
const FETCH_SUCCEEDED = 'parsnip/tasks/FETCH_SUCCEEDED';           1
const FETCH_FAILED = 'parsnip/tasks/FETCH_FAILED';                 1
const CREATE_SUCCEEDED = 'parsnip/tasks/CREATE_SUCCEEDED';         1
const FILTER = 'parsnip/tasks/FILTER';                             1

const initialState = {
  tasks: [],
  isLoading: false,
  error: null,
  searchTerm: '',
};

export default function reducer(state = initialState, action) {   2
  switch (action.type) {
    case FETCH_STARTED:
      return { ...state, isLoading: true };
    case FETCH_SUCCEEDED:
      return { ...state, tasks: action.payload.tasks, isLoading: false };
    case FETCH_FAILED:
      return { ...state, isLoading: false, error: action.payload.error };
    case CREATE_SUCCEEDED:
      return { ...state, tasks: state.tasks.concat(action.payload.task) };
    case FILTER:
      return { ...state, searchTerm: action.searchTerm };
    default:
      return state;
  }
}

export function fetchTasks() {                                    3
  return { type: FETCH_STARTED };
}

export function createTask({ title, description, status = 'Unstarted' }) {
  return dispatch => {
    api.createTask({ title, description, status }).then(resp => {
      dispatch(createTaskSucceeded(resp.data));
    });
  };
}

export function createTaskSucceeded(task) {
  return { type: CREATE_SUCCEEDED, payload: { task } };
}

export function filterTasks(searchTerm) {
  return { type: FILTER, searchTerm };
}

  • 1 Action-type constants can be shortened, given the context of the single file.
  • 2 The reducer is the module’s default export.
  • 3 Exports each action creator for use in containers

You don’t have to be quite this dogmatic when writing your own ducks files. For example, the lengthy naming convention of the action types, such as parsnip/tasks/FILTER, are meant to accommodate modules that may be packaged as npm modules and imported for use in other projects. At the risk of making assumptions, this likely doesn’t apply to your application.

Across various apps, these files go by more than a few names. In our experience, ducks and modules seem to be the most popular. We prefer modules, if only because the name is more intuitive, particularly for newcomers to the codebase; seeing ducks is sure to raise an eyebrow.

Let’s look at an example ducks project layout, shown in figure 11.5. Each file within the modules directory is a ducks file.

Figure 11.5. A project structure using ducks organization

Within that modules directory, the index.js file is a good location to handle importing each of the individual reducers and apply combineReducers.

11.3.1. Pros

The ducks pattern is a big win for scalability for a few reasons, not the least of which is the meaningful reduction in the number of files created. Whereas several of the domain-style patterns produce a file for each of the constants, reducers, and action creators, the ducks pattern results in one.

The ducks pattern also shares the domain-style pattern advantage of being able to find more feature-specific code in one place. Again, ducks go one step further, allowing you to find the relationships between action creators and reducers in a single file. Adding a feature is potentially as easy as changing a single file.

As mentioned, this pattern has the additional advantage of removing a small amount of boilerplate code, but more importantly, it lowers the mental overhead required to engage with a feature or absorb the context around a bug. When sagas are used to manage side effects, the action creators can become simplified to synchronous functions and make for more concise modules.

11.3.2. Cons

You’ll find tradeoffs for the advantages. The most obvious of which is that these module files will, by definition, become larger than any of the individual files they replace. Larger files go against the JavaScript trend toward greater modularity, and this will no doubt get under certain developers’ skin.

Like the domain-style pattern, if your application shares much state between domains, you may run into the same frustrations. Deciding where to draw lines between domains relies on tribal knowledge, which must be passed on to newcomers to the codebase. If you have actions being handled by multiple reducers, you have the same option to create a module for shared actions.

11.4. Selectors

Selector placement seems to be one item most of us can agree on: they belong either in the same file as their corresponding reducer or in a separate file in the same directory. Redux creator Dan Abramov encourages using this pattern to decouple views and action creators from the state shape. In other words, your container components shouldn’t have to know how to efficiently calculate the state they need, but they can instead import a function that does. Keeping selectors alongside reducers will also help you remember to update them if your reducer state changes.

11.5. Sagas

Sagas are useful for managing complex side effects and are covered in detail in chapter 6. When it comes to sagas, you’ll find a couple of commonly used organizational patterns. The first is to create a sagas directory within the src directory, as you did in chapter 6. This falls in line with the Rails-style organization pattern and shares the same advantages: predictability and familiarity.

A second option for handling sagas is to store them within the domain directories of a domain-style organizational pattern. This generally works well anywhere that the domain-style pattern is already a good fit. Earlier in the chapter, figure 11.3 demonstrated this pattern. You’ll find a saga.js file within the tasks directory.

In all our travels, we’ve never seen sagas implemented within a ducks module. It’s certainly possible, but by that point, there’s too much going on in one file. As mentioned in the earlier section, sagas are a popular way to slim down a ducks module. When all side effects are handled by sagas, the action creators typically become simple synchronous functions, keeping the ducks file nice and tidy.

11.6. Styles

Stylesheet location tradeoffs are similar to sagas. For the most part, your choices boil down to either co-locating them alongside the files they style or to stick them in a dedicated, Rails-like directory. Stylesheets co-located with their respective components are convenient to reference and import, but effectively double the size of a components or container directory. Only a couple scenarios exist in which you’d consider co-locating stylesheets: small prototypes or domain-style patterns that include components within the domain directory.

11.7. Tests

Tests have roughly the same options as sagas and selectors: either co-locate them alongside the files they test or stick them in a dedicated test directory. Frankly, there are great reasons to do either.

When storing tests in a dedicated directory, you’re free to creatively organize them as you see fit. The most popular options are to group them by file type (for example, reducers), test type (for example, unit tests), by feature (for example, tasks), or some nested combination.

Co-locating tests is an increasingly popular option. Like other co-located files, the boon is the convenience of referencing and importing a file in a peer directory. Beyond that, there’s significant value communicated by the absence of a co-located file: it’s quite clear that the component, action file, or reducer is untested.

11.8. Exercise and solutions

For this chapter’s exercise, take the time to refactor the Parsnip application from the Rails-style organization to a variation of the domain-style pattern or the ducks pattern. The “solution” to each of these options has been partially presented in the figures throughout the chapter. The goal of this exercise is to gain experience with, and to form stronger, more-informed opinions about, your available patterns.

You’re close but you haven’t stayed exactly true to the Rails-style pattern. You’ve left the App container in the root of the src directory and never circled back to address it. Let’s file that away in a containers directory and make sure to update the file paths of import statements in related files.

A couple of stylesheets are lying around in the src directory. Make the call whether they belong alongside their components or in a dedicated stylesheets directory. If you’re interested in co-location, move the App.css file into the container directory alongside App.js. If you’d rather clean up the root of the src directory a little more, go for the Rails-style directory captured in figure 11.2 earlier in the chapter.

Take a moment to weigh the resulting code structure. Is it an appropriate fit for this application? If you add a couple more features, will it still be? Which stress points would you be concerned about?

After the thought experiment, take a pass implementing a domain-style code pattern. Use figures 11.3 and 11.4 as guidelines. When you’ve completed the exercise, answer the following:

  • Does the application feel more intuitively structured?
  • If you were to add a new feature to bookmark or “favorite” tasks, is it clear where the new logic should end up?
  • How about if you wanted to add a feature to login or create a user account?

When you’re ready, give the ducks pattern a try. It’s not a far stretch from the domain-style pattern with containers abstracted out (figure 11.4). Move each of the domain-specific actions, constants, and reducers into a module, or ducks file.

  • Is the size of the file overwhelming or manageable?
  • Can you reduce the size of the file if it’s troublesome?
  • What are your early impressions of each strategy?

Admittedly, the number of decisions that you can make on this topic is exhausting. It merits a chapter, after all. This flexibility of Redux is a double-edged sword; you have the freedom and the burden to structure your application however you please. Fortunately, you only have to make most of these decisions once per application. If you want to dig deeper into this topic, check out the list of articles and resources found in the official documentation at http://redux.js.org/docs/faq/CodeStructure.html#code-structure.

In the next chapter, you’ll explore the use of Redux in other mediums outside of React in a web application.

Summary

  • Redux makes no demands on how you organize your code.
  • Several popular patterns exist for structuring code and files with a Redux application, each with their own tradeoffs.
  • Organizing code by domain or using the ducks pattern can improve your application’s ability to scale.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.200.242