Chapter 9. Best Practices

The Redux ecosystem as a whole is flourishing and extensive. Redux’s author, Dan Abramov, has put together some great documentation and the community has done an excellent job growing the Redux ecosystem. However, a lot of knowledge is still spread across many examples, tutorials, Github issues, and engineers. In this section we will cover a condensed version of useful lessons, FAQs, and tips learned from the trenches, and guide you around common questions and pitfalls when scaling a non-trivial real world Redux application. We will tackle the following:

  • Actions
  • Reducers
  • Selectors
  • Middleware, Store Enhancers
  • Project Structure
  • UI, Rendering
  • Developer Tooling and Debugging
  • Testing

Actions

In real world applications, simple synchronous actions are not enough to handle everything your app will do. You’ll likely need to handle async behavior, deal with promises, trigger multiple actions, or dive into more complex recipes. Although there’s no specific rule for how you should structure your actions and action creators, here are some general insights.

Disambiguation

  • A Redux action is a plain old JavaScript object (POJO). It consists of a type field and optional data (often stored in a payload field) that describes a change for your application.
  • An action creator is a function that creates an action. They do not dispatch to the store (unlike Flux), yet they return action objects and may contain additional logic to prepare an action object.
  • Actions are the only way to get data into the store. Pass the result of an action creator to the dispatch() function to trigger a state change.
  • Redux’s bindActionCreators turns an object whose values are action creators, into an object with the same keys, but with every action creator wrapped into a dispatch call so they may be directly invoked.
  • Redux thunk is an extremely useful middleware that uses thunks to solve common problems you’ll likely encounter. It allows you to write action creators that return a function instead of an action. You can use thunks to delay the dispatch of an action, or to dispatch only if a certain condition is met. They are also used when creating Async Action Creators.

Actions should be serializable

Serializable actions are at the heart of Redux’s defining features and enable time travel and replaying actions. It is okay to use Promises or other non-serializable values in an action as long as it is intended for use by middleware. Actions need to be serializable by the time they actually reach the store and are passed to the reducers.

Defining and organizing action types

Action types should be defined as self-descriptive string constants helping to reduce repetitiveness, reproduce issues, and facilitate debugging.

Defining types in a constants module can help encourage you to think early on about how you want to organize your app. This can act as a manifest for all of the things that can occur in your app. This can be extremely useful when discussing the app with other devs (use the action list as a reference), or with other teams, such as discussing actions with your UX or analytics team.

Organizing action types by export:

    // constants/ActionTypes.js
    export const ADD_NOTE = 'ADD_NOTE';
    export const DELETE_NOTE = 'DELETE_NOTE';
    export const FAVORITE_NOTE = 'FAVORITE_NOTE';

Organizing action types with keyMirror:

    // constants/ActionTypes.js

    import keyMirror from 'keymirror';

    const ActionTypes = keyMirror({

        NOTES_REQUEST: null, // yes! less typing
        NOTES_SUCCESS: null,
        NOTES_FAILURE: null

    });

    export default ActionTypes;

The advantage to organizing your action type constants with keyMirror is to reduce the likelihood of typos. The keyMirror module is also used in many traditional Flux examples (by Facebook), and perhaps may be familiar to you. If you are migrating a traditional Flux app, you do not have to change the way your action types are organized.

The downside is that it becomes a little more difficult to import your constants individually using ES6, since you’ll have to import the entire object that was returned by keyMirror. Another downside is that it becomes more difficult for static analysis and IDEs to track down where constants are defined.

To organize action types along side your action creators:

    // actions/ApiActions.js

    // not going to forget to declare these constants in another file
    export const NOTES_REQUEST = 'NOTES_REQUEST';  
    export const NOTES_SUCCESS = 'NOTES_SUCCESS';
    export const NOTES_FAILURE = 'NOTES_FAILURE';

    export function fetchNotes() {

        return (dispatch) => {

            dispatch({ type: types.NOTES_REQUEST });

            return fetch(url, options)
                .then(parseResponse)
                .then(checkStatus)
                .then(normalizeJSON);
                .then((json) => dispatch({ type: types.NOTES_SUCCESS, payload: json }))
                .catch((error) => dispatch({ type: types.NOTES_FAILURE, payload: error, error: true }));

        };

    }

The advantage to organizing your action constants next to your action creators is to keep your code condensed with minimal duplication. This becomes useful if your app is fairly small, with only a small amount of action types. It also becomes easier to reference these constants when editing your action creators. The downside depends on the size of your application. If you have a lot of actions or complex action creators, such as middleware, it may be better to split your constants into a separate folder and/or files for easier reuse and smaller action creator file sizes. Of course, it depends on your app and your preferred file/folder structure.

Another downside to this organization is that your reducers need to access action constants defined next to your action creators. If you want to prevent duplication, they need to be exported, and your reducers would then import them directly from your action creator modules. At this point, perhaps it is easier to keep them in a seperate file.

In order to import your constants:

    // actions/ApiActions.js
    import ActionTypes from '../constants/ActionTypes';

    const { NOTES_REQUEST, NOTES_SUCCESS, NOTES_FAILURE } = ActionTypes;

    //  Will I end up duplicating anyways?

    function createFetchActionCreator(options, types){
        //deconstruct
        const [NOTES_REQUEST, NOTES_SUCCESS, NOTES_FAILURE] = types;
        //...
    }

    const fetchNotes = createFetchActionCreator(options, [NOTES_REQUEST, NOTES_SUCCESS, NOTES_FAILURE]);
    // ...
    dispatch(fetchNotes());
    // actions/ApiActions.js
    import ActionTypes from '../constants/ActionTypes';
    // is the entire object needed?
    // ...
    dispatch({ type: ActionTypes.NOTES_REQUEST });

    // Does repeating the object become ugly?
    // actions/ApiActions.js
    import { NOTES_REQUEST, NOTES_SUCCESS, NOTES_FAILURE } from '../constants/ActionTypes';

    // ...
    dispatch({ type: NOTES_REQUEST });

    // this is specific

Keep in mind that the way you organize your constants can make it easier to import. Consider the fact that you may have to import in several places, such as within your custom middleware and your reducers. Depending on how you organize these, you may only want to import one or two action types, as opposed to an entire object, and benefit when optimizing by being specific on what action types are actually used within your module.

Action naming conventions

Naming actions is extremely important, because your actions describe what your app can do and what your action does. As your app grows, the easier it is to understand what an action does, and the easier it will be to quickly traverse your application. When naming actions (the action type constant and action creator) it can be helpful to derive the name of an action directly from a sentence that describes what you want your app to do (creating statements of functionality).

//When the page loads, we want our app to Fetch notes
//becomes:  FETCH_NOTES, dispatched on page load

//When a user clicks the checkbox it will Filter the notes list
//becomes:  FILTER_NOTES, dispatched when user checks the checkbox

//The radio button will Toggle the visibility of favorited notes
//becomes:  TOGGLE_NOTES_VISIBILITY, dispatched when the radio button is toggled

Avoid going beyond three segments in your action name. This may be a first sign that something else in your app needs to be fixed or addressed. Make sure that it is clear on what your action does. Being too vague can be painful for other developers jumping into the project. If you are doing it right, you should not have to comment or explain your actions.

Good:

    export const ADD_NOTE = 'ADD_NOTE';

Bad:

    export const NOTE = 'NOTE'; // fine, we have notes, but am I adding, deleting, or favoriting?

Bad:

    export const ADD = 'ADD';  // add what?

Be careful not to use terms that may mean something else within your app.

Good:

    export const RESET_ERRORS = 'RESET_ERRORS';

Bad:

    export const RESET_STATE = 'RESET_STATE';  // reset the entire state tree?  or just the errors?

Your actions should be specific and typically only describe doing one thing at a time. This makes it easier to understand, and follows the single purpose rule, making it easier for add/deleting and refactoring. Keep in mind that you may have actions that result in multiple things occuring. Multiple reducers can listen to the same action and the intention of the action may actually impact multiple parts of your state; however, we should still aim to name our actions in a singular manner to avoid confusion and to maintain consistent verbage.

Good:

    export const DELETE_NOTE = 'DELETE_NOTE';
    export const SHOW_FORM = 'SHOW_FORM';

Bad:

    export const DELETE_NOTE_AND_SHOW_FORM = 'DELETE_NOTE_AND_SHOW_FORM';

Good:

    export const LOAD_PROJECT = 'LOAD_PROJECT'; 
    // handled my multiple reducers and many things occuring when dispatched

Bad:

    export const LOAD_PROJECT_AND_LOAD_ENTITIES = 'LOAD_PROJECT_AND_LOAD_ENTITIES';  
 // do not describe implementation details or what your reducers are expected to do in the action name.  Describe the intention of the action.  If it is not clear, consider introducing another action.

Creating Actions, type with flags vs multiple action types

It is important to decide on a convention early on with your team.

You may be inclined to reuse an action or a type for multiple things and attach a dedicated status field in your actions (Bad):

{ type: 'FETCH_POSTS' } //bad, not sure if this is the start or end of async flow, need to type check
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' } //bad, now my reducer needs to figure out more, and this isn't a standard pattern
{ type: 'FETCH_POSTS', status: 'success', response: { ... } } // does not follow Flux Standard Action, may need to normalize in multiple places

Although this does work for trivial applications, it can become confusing as you scale, especially during debugging. Imagine what your Redux devtools list/logs will look like with these action types. You now have to expand and examine every single action every time this action is dispatched to determine if it is the start, error, or success. It may cause more conditions and type checking within your reducers, making it likely that you will need to duplicate, especially if logic in your actions are used in multiple reducers. It also becomes more difficult to write middleware or reuse other’s middleware because it does not follow a standard pattern.

You can and should define separate types for distinct actions, even if it feels verbose. Multiple types leave less room for a mistake, make it easier to debug, and reduce ambiguity (Good):

{ type: 'POSTS_REQUEST' } // nice, I can use this to toggle a loader on
{ type: 'POSTS_FAILURE', error: true, payload: new Error('oops...') } // i can check the error field within my error handling reducer very easily, I also have access to the error
{ type: 'POSTS_SUCCESS', payload: { ... } } // if i only care about handling data, I know exactly what action to listen to, in my middleware I can also spot types postfixed with _SUCCESS

As your app grows it is very obvious what each type does. It is also very easy to reuse the action across multiple reducers and easily understand them as context may be different in different reducers. For instance, POSTS_SUCCESS in your posts.js reducer may be used to update your list of posts in the post list component. It may also be used in the navigation reducer and navigation component to update the count pill on the navigation. Finally, remember that others will likely be working on the project, so defining your naming conventions early on can facilitate when devs copy and paste, or need to quickly ramp up on a new section of code.

Examining the types:

{ type: 'POSTS_REQUEST' }

An action informs reducers that the request began. Reducers can use this to toggle on a loader (isFetching flag in your state).

{ type: 'POSTS_FAILURE', error: true, payload: new Error('oops...') }

An action informing reducers that the request failed. Reducers can use this to reset isFetching state or display error messages.

{ type: 'POSTS_SUCCESS', payload: { ... } }

An action informs the reducers that the request finished successfully.

Use Flux Standard Actions (FSA’s)

Although it is not required, as actions only require a type, it is recommended to use the Flux Standard Action format for your actions (also called FSA’s). FSA’s define simple guidelines for a common format. See the official documentation: https://github.com/acdlite/flux-standard-action

It is much easier to debug and work with actions if we use a common format. Using a common format can facilitate sharing code or extracting logic into middlewares. We can also use the flux-standard-action module to ensure our actions are compliant.

In a real world app we can create FSA compliant actions by convention, and use FSA’s isFSA helper function in our tests for verification. FSA’s are extremely useful when creating middleware. Because we are using a standard, we can create middleware that depends on particular action properties and write reliable and efficient code to handle these properties. A common example is intercepting meta data attached to our actions within Analytics Middleware mentioned in previous chapters.

Sharing state between two reducers

If you need to share data between two reducers, the fastest (typical way) is to use Redux thunk and access state through getState(). An action creator can retrieve additional data and put it in an action so that each reducer has what it needs to update its own slice of state. Notice that the inner function receives the store method dispatch and getState as its parameters.

function addNote(){
  return (dispatch, getState) => {  
  // i can get needed state for my action to make sense without passing more props

    // i have access to the entire state, if i wanted, i could reuse a selector here
    const { name } = getState();

      dispatch({
          type: ADD_NOTE,
          payload: {
              note: 'my note',
              author: name
          }
      });
  };
}

store.dispatch(addNote());

General insights:

  • There are additional ways to share data between two reducers. Another common method is to use top level reducers that have access to other reducers. Depending on your needs, this may be a better choice.
  • Perhaps it is better to provide your component with the necessary props and pass to your action creator if they need them anyways.
  • Depending on your preference, you may not want your action creators to know too much. In this case, you may have to opt for an alternate technique.
  • Always be concious of timing issues when coordinating state between multiple reducers.
  • See the official Redux FAQ for more indepth information on sharing state between reducers: http://redux.js.org/docs/FAQ.html#reducers-share-state.

Performing conditional dispatch logic with thunks

Generally, reducers contain the business logic for determining the next state. However, reducers only execute after actions are dispatched. Thunks allow you to read the current state of the store and subsequently perform conditional dispatches before your actions reach your reducers. It is best to use this pattern if you are not able to perform the logic within your reducers, or you would like to extract out complex logic from your view.

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();
    if (counter % 2 === 0) {
      return;
    }
    dispatch(increment());
  };
}

store.dispatch(incrementIfOdd());

General insights:

  • It can be better to handle this type of logic in your reducer, depending on other state and where you want to put your logic. Perhaps you have multiple reducers that need to listen to an intended increment action and handle accordingly.
  • Leveraging action creators (thunks) to solve complex logic is often an easy choice and very helpful when coordinating issues where timing is critical.
  • There is not a hard and fast rule that says all your business logic needs to be contained only in your reducers, or only in your actions. A mixture of both, depending on your preference or needs, is acceptable.

Tips

  • You can use action creators to transform API data. Using action creators to transform UI data to what makes sense for your APIs is common. This works for both requests and responses. Because your reducers only receive actions, they are unaware of how the action was created. You will likely have to deal with legacy APIs, APIs out of your control, poorly designed APIs, or APIs not specific to your application. In these cases the responsability can be given to action creators to transform responses and requests of an API call to make sense for your reducers.
  • Action Creators can be asynchronous (think redux-thunk middleware).
  • Since your application will require asynchronous updates, business logic or conditional logic may end up in your action creators, however, remember that actions need to be serializable by the time they actually reach the store and are passed to the reducers.
  • Although it is okay for your reducers to contain logic that transforms your data, you will want to avoid duplication depending on the action and data. If multiple reducers need to receive a payload that is ready or you find your reducers caring about other slices of state than it’s own, put the logic to prepare the data in your action creators, not your reducers.
  • Action creators can share selectors, because they have access to the complete state with thunks. Reducers often can’t share selectors, because they only have access to their own slice of state depending on how they are organized.
  • Using thunks, action creators can dispatch multiple actions, making complicated state updates easier and encourages code reuse.
  • Without middleware, Redux only supports synchronous data flow. Async middleware (redux-thunk, redux-promise) wraps dispatch() and allows you to dispatch something other than actions, for example, functions or Promises. You can use Redux thunk and Redux Promise together; they are not mutually exclusive.
  • Putting constants in a separate file allows you to check your import statements against typos to prevent accidentally using the wrong strings.
  • If you find yourself duplicating code accross multiple action creators, consider condensing the logic into middleware.

Reducers

We use reducers to shape and organize our state. When you embark on more complex applications, designing and organizing your reducers is not always trivial. In previous chapters we covered the art of designing your state and how to dissect UI state and domain specific state slices. Here are some tips that can help facilitate creating your reducers and keeping your state design clean.

General insights:

  • Reducers take the previous state and an action, returning the next state. This is an important understanding when deciding how to handle data and data mutations within your reducers.
  • Reducers are synchronous pure functions with no side effects, this makes them extremely predictable and easily testable.
  • Typically, reducers group the various slices of application state and become the top level keys within your state tree.
  • There is not a hard and fast rule on where business logic should reside (inside action creators or inside reducers), but the key is to find a balance that makes sense for your application.
  • Reducers do not need to exclusively return objects. They can return arrays, numbers, strings, or whatever object shape that makes sense for a particular reducer (slice of state).

Reducer initial state

Creating a reusable initialState const can be useful for reuse or returning back to the initial state of the reducer. A well-defined initialState can also act as a psuedo schema for your reducer. In other words, you create your intialState const and all possible properties with default values, allowing it to act as a point of reference for what your resulting state will look like. This is particularly useful for other devs jumping into the project, because they can glance at the initial state before diving into complex logic. Of course, sometimes this is not always possible, but it is good to do if possible. Exporting the initialState const can also be useful to import into your tests.

export const initialState = {  // I know exactly what my reducer will change, I also export for use in my tests
    isFetching: false,
    notes: [],
    message: 'Your notes list is empty, click to fetch them.',
    numberOfVisibleNotes: 0,
    author: null  //updated when user inputs into field
};

Eliminating switch statements

Common reducer examples use the switch statement to handle actions. This is not a requirement, as it is okay to use if statements or use other conditional logic that matches by something other than type (such as logic based on payload data).

const initialState=[];
// ...
function meta(state = initialState, {
    type,
    payload,
    meta
}) {

    if (meta.something) {
        return state.concat(payload);
    }
    return state;
}

There are packages such as redux-actions that contain helper methods to facilitate in eliminating large switch statements. In general, if you find your reducers with extremely large switch statements, consider extracting logic into helper functions or dividing your reducer into multiple smaller reducers.

Tips

  • Reducers are just functions, so you can use functional composition and higher-order functions to compose your reducers.
  • redux-actions package contains several helpers to simplify creating actions and reducers. This may be a viable addition depending on your needs. https://github.com/acdlite/redux-actions#handleactionsreducermap-defaultstate.
  • It is perfectly acceptable to detach a reducer from a store. Reducer functions could be used in other places besides a Redux store. For example, you could use one to calculate updated local state for a React component.
  • Common practice is to divide state into multiple slices or domains by key, with a seperate reducer function to manage each slice. Consider naming reducers by function, not the domain they serve.
  • Data from the server (an API) is not necessarily your final model. Your final state tree will be composed of multiple data inputs. It is okay if your reducers do not align perfectly with the structure of your API(s).
  • Splitting your store into multiple smaller reducers allows for separation of concerns and can also eliminate large switch statements. It can also facilitate testing and reduce cognitive overload when traversing your application.
  • Multiple reducers can listen to the same action.
  • Using multiple smaller reducers allows us to use libraries (such as Immutable.js) on a per reducer basis; if you do not need a particular library for your entire state tree, you don’t have to use it for every reducer. This can help with refactoring, transitioning to new libraries, or porting over functionality from other applications.
  • Naming your reducer files the same as your reducer key can help facilitate debugging and quickly finding troublesome logic.
  • Reducers should not dispatch actions.
  • If you have reducers that need to depend on other state, you can pass it explicitly as the third argument to your reducers.
  • You can use combineReducers() multiple times, since it returns a reducer with (state, action) => state signature, just like a standard reducer.
  • It is okay to use additional functions in the reducer structure to help break things into smaller pieces.
  • If you have deeply nested data structures, consider using libraries such as normalizr or redux-orm, see the official Redux FAQ for more information: http://redux.js.org/docs/FAQ.html#organizing-state-nested-data

Selectors

A selector is a function that selects part of the state tree. Selectors are also commonly used to return data that is derived from the state.

Tips

  • Redux state should always be your source of truth.
  • Generally, it is not a good idea to put derived data (such as filtered state) in the Redux store. Using selectors to compute derived data allows for reuse across components and also allows Redux to store the minimal possible state
  • Selectors simplify testing and facilitate extracting complex logic out of your components.
  • Reselect is a recommended selector library to write performant selectors. It is extremely powerful when projects have performance problems because of many derived computations causing multiple re-renders. Reselect creates memoized functions that will return the last value without recalculating if called with the same arguments multiple times in a row.
  • Colocating selectors with reducers helps organize related logic. A common practice is to make your default module export your reducer, and a prefix named selector exports with get.
  • Official Reselect docs: https://github.com/reactjs/reselect.

Middleware, store enhancers

Middleware provides a third-party extension point between dispatching an action and the moment the action reaches the reducer.

A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator. This is similar to middleware in that it allows you to alter the store interface in a composable way.

Tips

  • Use middleware to handle common side effects like analytics tracking.
  • Redux thunk has a helper withExtraArgument so you can inject extra arguments allowing you to mock your async API making writing unit tests easier. This can be useful to inject common functionality.
  • You can use middleware to validate state across reducers, creating a single point that can analyze your entire state tree.
  • More on store enhancers: https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer.

Project Structure

While Redux itself has nothing to say about file structures (as it should depend on your specific needs), there are a number of common patterns for organizing the file structure of a Redux project. Each pattern has pros and cons. Let’s explore these pros and cons.

Organizing by Concept Type

The most common way is to organize your project by concept. This is simply arranging the application files by function. Each concept lives under a common parent.

├── src                     # source
│   └── styles              # css/sass/less
│   └── images              # .png,.svg,.gif etc.
│   └── js                  # react/redux source
│     ├── actions           # redux actions
│     ├── components        # react components/containers
│     ├── constants         # constants, action types
│     ├── reducers          # reducers
│     ├── selectors         # selectors, using reselect
│     ├── store             # store, w/devtools & prod config
│     ├── utils             # utilities/helpers
│     └── index.js          # app entry
│   └── index.html          # app shell
├── test                    # tests
│   └── *.js                # specs
│   └── setup.js            # test config
├── webpack                 # webpack
├── .babelrc                # babel config
├── .eslintrc               # eslint config
├── .gitignore              # git config
├── LICENSE                 # license info
├── package.json            # npm
└── README.md               # installation, usage

Pros

  • Easy organization
  • Follows many tutorials and examples
  • Easy to get started on trivial applications
  • Does not couple types
  • Those new to a project may initially find it easier to traverse
  • No duplication in file names

Cons

  • Repetition of module groupings/names
  • Splitting up application for optimization is more difficult
  • No group of related features
  • Naming features can be more difficult
  • Editing a feature means traversing multiple files/folders

Organizing by Feature or Domain

Another common way to organize your project is by arranging your files into feature folders, grouping everything related to a specific feature.

├── src                             # source
│   └── notes                       # feature
│     ├── Notes.js                  # component/container
│     ├── Notes.test.js             # tests
│     ├── NotesActions.js           # actions
│     ├── NotesActions.test.js      # tests
│     ├── notesReducer.js           # reducer/selectors
│     └── notesReducer.test.js      # tests
│   └── comments                    # feature
│     ├── Comments.js               # component/container
│     ├── Comments.test.js          # tests
│     ├── CommentsActions.js        # actions
│     ├── CommentsActions.test.js   # tests
│     ├── commentsReducer.js        # reducer/selectors
│     └── commentsReducer.test.js   # tests
│   └── index.html                  # app shell
├── webpack                         # webpack
├── .babelrc                        # babel config
├── .eslintrc                       # eslint config
├── .gitignore                      # git config
├── LICENSE                         # license info
├── package.json                    # npm
└── README.md                       # installation, usage

Pros

  • Feature naming is clear
  • Architecture/structure independent
  • Trivial to split code (lazy load features based on routes or conditions)
  • Easier to pluck features out of application for reuse
  • Allows for feature encapsulation and ability to expose API using a root index.js to export your reducers, action creators, and selectors

Cons

  • Groupings must be repeated
  • May be overkill depending on project size
  • Still requires central store configuration
  • May encourage mapping your data to a single feature
  • Not everything may belong to a specific feature
  • Sharing functionality accross features may be ambiguous and awkward

Alternatives

Depending on your project’s complexity, you may want to choose to group your actions and reducers in one file. It is a slight variation that may help readability, particularly in smaller projects. The community already took note of such a pattern and Erik Rasmussen summarised the approach under the concept of Ducks https://github.com/erikras/ducks-modular-redux. According to him, it makes more sense for these pieces to be bundled together in an isolated, self-contained module which can be easily packaged into a library.

General Insights

  • Most of the time components are not used outside of containers.
  • ES6 allows to export multiple items, which allows you to merge two things into one file and use export default as the container and regular export the component.
  • Configuring test runners is trivial for both patterns.
  • Reducers rarely correspond to specific features/pages in a clear way.
  • Choose the pattern that makes sense for your needs, there is no right or wrong way to organize your application, consistency is key.
  • Your project structure is usually different from state shape, do not feel compelled to shape your state tree based on your feature/file organization.
  • Focus on structuring your reducers, actions, and action creators well. If needed, they can always be moved to alternate project structures.

UI, Rendering Tips

  • react-redux’s Provider HOC (higher order component) wraps a root component and makes it possible to use connect()
  • When using react-redux, all commonly discussed react optimizations still apply
  • A good pattern to adopt is the ‘Presentational and Container’ component pattern. https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0.
  • As your app grows/matures the components that are connected to Redux are likely to change. Embrace the idea that you may need to refactor later on and eliminate container components, or connect child components.
  • Prefer smaller stateless functional components when possible.
  • There is not a hard and fast rule that says every piece of state should belong in the Redux store, it is okay for components to manage thier own state (using reacts state management).
  • recompose is a handy set of React tools to optimize your components https://github.com/acdlite/recompose.
  • onlyUpdateForKeys from recompose can be used to make sure only updates occur if specific prop keys have changed
  • If you need to apply several HOCs on a component you can use Redux’s compose() as follows:
import { connect } from 'react-redux';
import { compose } from 'redux';

class MyComponent extends Component {
    // ...
}

const enhance = compose(
    onlyUpdateForKeys(['propA', 'propB']),
    connect(mapStateToProps, mapDispatchToProps)
    // anything else
)

export default enhance(MyComponent)

Developer tools

The two most useful tools for developers and debugging Redux applications are custom logging middleware and the redux-devtools-extension package for Chrome, Firefox, and Electron. These tools will cover your needs 99% of the time. The custom logging middleware allows you to tweak your logging as you go and has a simple, small footprint.

The redux-devtools-extension allows for minimal configuration to enhance your Redux store and leverages the power of several existing Redux DevTools extensions. This makes it easy to implement and easier to maintain (it is not directly coupled to your application code).

Custom Logging middleware:

// logger.js
export default (store) => (next) => (action) => {
    console.groupCollapsed(action.type);
    console.info('action:', action);

    const result = next(action);

    console.debug('state:', store.getState());
    console.groupEnd(action.type);

    return result;
};

Redux DevTools Extension: https://github.com/zalmoxisus/redux-devtools-extension

// configureStore.js
import {createStore, applyMiddleware, compose} from 'redux';
import rootReducer from '../reducers';
import thunk from 'redux-thunk';
import logger from '../middleware/logger';
const IS_BROWSER = typeof window !== 'undefined';
const IS_DEV = process.env.NODE_ENV !== 'production';
const middleware = [thunk];

if(IS_DEV){
    middleware.push(logger);
}

export default function configureStore(initialState = {}) {

    return createStore(rootReducer, initialState, compose(
        applyMiddleware(...middleware),
        IS_DEV && IS_BROWSER && window.devToolsExtension ? window.devToolsExtension() : (f) => f
    ));

}

For additional developer tools & resources, checkout out: https://github.com/markerikson/redux-ecosystem-links/blob/master/devtools.md

Testing Tips

  • For async action creators, it is best to mock the Redux store, so redux-mock-store is extremely useful.
  • When testing action creators, we want to test whether the correct action creator was executed and also whether the correct action is returned.
  • When using thunks with promises, you should return your promise for testing. If you are using redux-thunk and fetch, it is recommended to return your fetch promise from your action creator.
  • If you have adopted a standard action shape (perhaps Flux Standard Action), you may test that your action creators follow the standard. You may also want to enforce FSA’s with middleware.
  • Testing a reducer should always be straight forward, reducers simply respond to incoming actions and turns the previous state to a new one. If you find testing reducers difficult, it is likely you are doing something wrong within your reducers.
  • In general, if we avoid using large reducers and compose our final store with multiple smaller reducers, our tests become easier to organize in addition to the benefits outlined in the Reducer best practices section.
  • Generally having 1 test file for every 1 reducer file makes it easier to navigate your tests as your app grows. It also allows you to reorganize your project if needed. For example, perhaps you decide to refactor your app and move your reducers into modules, you can simply move the corresponding test.
  • You can export an initialState value from your reducer modules to reuse in your tests.
  • Use action creators within your reducer tests to avoid duplication of action objects.
  • When testing mapStateToProps(), extract behavior from mapStateToProps() and move into selectors, since selectors are easier to test.
  • Selectors should be pure functions.
  • mapDispatchToProps() is usually not worth testing.
  • For more testing resources, checkout https://github.com/markerikson/react-redux-links/blob/master/react-redux-testing.md.

Conclusion

The tips in this chapter will help you as you implement your own Redux applications. In the next and final chapter, we explore the topic of going offline.

..................Content has been hidden....................

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