Real-World ReactJS and Redux, Part 2

When this happens do that… or side effects!

This is the second in a series of blog posts about real-world ReactJS usage and what we’ve learned scaling our app at Threat Stack.

In this post, we’ll be displaying actions based on an api middleware shown in Part 1.

Still making sure to answer these questions:

  • What is the ease of development?
  • How fast can new team members understand what’s going on?
  • How fast can you figure out where something is broken?

Sometimes a seemingly simple feature request comes through that looks like this:

If X happens do Y, but only if state looks like Z.

Since you want to maintain your code decoupled between react (view) and redux (data management), simple requests like this end up seeming more difficult than they should be.

A good example is client-side tracking.

At some point you’ll want, nay, need to track when a user clicks on something and pass how many items are in display or other information about the current state.

The first pass at this was an analytics middleware.

Things were based on a property aptly named “analytics”.

So the action looked like this:

return {
  types: [ MARK_READ, MARK_READ_SUCCESS, MARK_READ_ERROR ],

  callAPI: () => Api.updateTodos({
    ids,
    isRead: true 
  }),

  analytics: {
    [MARK_READ_SUCCESS] : {
      idsCount: ids.length,
      isSuccess: true
    },
    [MARK_READ_ERROR] : {
      idsCount: ids.length,
      isSuccess: false
    }
  }
}

 

So far… not horrible.

Depending on the event type (success/error), we can track different things.

But a new requirement comes in and… You won’t believe what happened next.

If it’s successful, we also need to redirect the user to a different section.

return {
  types: [ MARK_READ, MARK_READ_SUCCESS, MARK_READ_ERROR ],

  callAPI: () => Api.updateTodos({
    ids,
    isRead: true 
  }),

  analytics: {
    [MARK_READ_SUCCESS] : {
      idsCount: ids.length,
      isSuccess: true
    },
    [MARK_READ_ERROR] : {
      idsCount: ids.length,
      isSuccess: false
    }
  },
  
  redirect: ({ state }) {
    // Nope... Nope... Nope... I'm gonna stop right here
  }
  
}

 

We made another middleware that looked for a specific property.

We sure did.

But that won’t scale.

We need something more generic that can scale for different use cases.

A Super-Handy™ Generic Middleware

Scenario:

if I'm filtering a list of todos  
  If I mark all as read   
      clear the filter   
  If I mark only some of them as read 
     leave the filter on
     
OnError show an error notification

 

Rules:

  • This logic shouldn’t live in the view.
  • This is based on the action of marking a todo.
    If I add a different version of this component, an A/B test for instance, state should be updated, and everything should just work.

component.react.js

handleMarkRead (ids) {
    dispatch(markReadTodos(ids));
}

 

actions.js

import { clearTodosFilter } from '../filterActions';
import {
    MARK_READ,
    MARK_READ_SUCCESS,
    MARK_READ_ERROR
} from '../constants'

export function updateItem (ids) {
  return {
    types: [ MARK_READ, MARK_READ_SUCCESS, MARK_READ_ERROR ],

    callAPI: () => Api.updateTodos({
        ids,
        isRead: true 
    }),

    effect ({ dispatch, state, type }) {
      if (type === MARK_READ_ERROR) {
        // dispatch a notification action which should update
        // state and show a notification somewhere 
        dispatch(showErrNotification(
         'There was an error updating your todos'
         ));  
      }
      
      if (type === MARK_READ_SUCCESS) {
        const { todosById } = this.state;
        let hasReadAll = true;
        for ( const id in todosById) {
          if (!todosById[id].isRead) {
            hasReadAll = false;
            break;
          }
        }
        
        if (hasReadAll) {
          dispatch(clearTodosFilter());
        }
      }
    },
  };
}

 

Scenario:

When the user updates an item, show an error notification if it fails.
If it succeeds, show a different notification, but only if the item is FOO or some other state changed.

 

Item.react.js

handleUpdateItem (item) {
  dispatch(updateItem({ 
    item
  });
}

 

ItemActions.js

  1. Update Item call will happen.
  2. App State will be updated.
  3. If there’s an error:
    – We’ll dispatch an error notification.
  4. If it was successul and the current changes count (based on current state) is not valid, we’ll dispatch a different notification.
import { showErrNotifcation } from '../notificationActions';
import {
    UPDATE_ITEM,
    UPDATE_ITEM_SUCCESS,
    UPDATE_ITEM_ERROR,
    MAX_CHANGES
} from '../constants'

export function updateItem ({ item, prevItem }) {
  return {
    types: [ UPDATE_ITEM, UPDATE_ITEM_SUCCESS, UPDATE_ITEM_ERROR ],

    callAPI: () => Api.updateItem(item),

    effect ({ dispatch, state, type }) {
      if (type === UPDATE_ITEM_ERROR) {
        // dispatch a notification action which should update
        // state and show a notification somewhere 
        dispatch(showErrNotification('Error updating item'));  
      }
      
      if (type === UPDATE_ITEM_SUCCESS) {
        const { itemsById } = this.state;
        
        if (itemsById[item.id].changes === MAX_CHANGES) {
          dispatch(showNotification(
            `You can no longer alter this item`
           )); 
        }
      }
    },
  };
}

 

And the Middleware…

effectsMiddleware.js

export default function effectsMiddleware ({ dispatch, getState }) {
  return next => action => {
    const nextValue = next(action);

    // if we have an effect function 
    // we'll run it before carrying on 

    if (!action.effect) {
      if (!isFunction(action.effect)) {
        throw new Error('Expected effect to be a function');
      }

      // pass back useful things
      // NOTE: if you put this middleware *after* the `callAPIMiddleware`
      // you could also pass back `action.data` (the server response)
      action.effect({
        dispatch : dispatch,
        state    : getState(),
        type     : action.type,
      });

      delete action.effect;
    }

    return nextValue;
  };
}

 

You’ll want to make sure it is placed after your other custom middlewares, however.

 
const middleware = process.env.NODE_ENV === 'production' ?
  [ thunk,  callAPIMiddleware, effectsMiddleware ] :
  [ thunk, callAPIMiddleware, effectsMiddleware, logger() ];

 

Where We Ended Up . . .

  • Logic for sub actions is with the actions code.
  • Since we’re dispatching other sub actions, you can grep for action names, and they’ll be displayed on your console for debugging.
  • You’ll be able to follow a flow for actions happening in your code in the console.

 

P.S. Hack the Planet