This is the second in a series of blog posts about real-world ReactJS usage and what we've learned about scaling our app at Threat Stack.
In this post, we'll be displaying actions based on an api middleware shown in Part 1.
Getting Everyone on the Same Page
It is critically important to ensure that all team members are on the same page at all times when it comes to the kind of products and applications that are being created. ReactJS is something that will require somewhat of a learning curve, but it can be surmounted once people have had the time that they need to get it figured out.
It is fair to say that it is not always the easiest thing in the world to learn a brand new system, but many people have picked up on how to make ReactJS work for them, and we want to recognize those individuals and make it abundantly clear that it is something that can be accomplished.
Everyone should want to learn what they can about ReactJS anyway as it is likely to become a much larger part of their lives very soon.
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 the 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 }) {
}
}
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, the 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(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
Update Item call will happen.
App State
will be updated.
If there's an error:
– We'll dispatch an error notification.
If it was successful 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(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 (!action.effect) {
if (!isFunction(action.effect)) {
throw new Error('Expected effect to be a function');
}
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 . . .
The 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.
Comments