This is the beginning of a series of articles on the construction of a fully featured task calendar in React and Flux. The calendar uses React Router and connects to an API to pull/update events. In the series as a whole I'll go through the overall structure of the calendar as well as each indiviudal component. I'll also discuss issues that came up as I was making the calendar, and how I resolved them.
You can see a finished version of the calendar here.
While you need not have built an app with react to follow along, you should have a decent understanding of javascript and be familiar with what react is. If you'd like a deeper introduction to react, check out part one of Tyler McGinnis' series on react and then come on back.
Part one focuses on setup and the overall structure of the calendar and goes through an explanation of how the components fit together to make the entire application. We'll also touch on how the calendar retrieves events from the API. Part two looks at the individual components and views. Part three focuses on the actions, including the search and filtering of events. Finally, part four contains closing thoughts on the app as well as a summary of the topics covered.
1. Setup
Gulp and Dependencies
You can find the repo for the finished app here.
Before cloning this project, make sure that you have node.js installed. You will also need to have gulp installed globally. To do this, after installing node, open your terminal and run:
npm install --global gulp
Then, to get started, navigate to where you would like your project folder to be and run:
git clone https://github.com/hilary-L/react-cal-with-api
cd react-cal-with-api
npm install
This will install all of the dependencies for the project and get us ready to put in our react components. Gulp will process our JSX into javascript using browserify/reactify, and will compile our scss files using gulp-compass. The gulp workflow in this project is based off of the workflow demonstrated by part two of Tyler McGinnis' series, but has been modified to include tasks to take care of our stylesheets. I won't be going over how the gulpfile works to process our files, as it's outside the scope of this article. If you would like more information on how to use gulp, browserify, and reactify for your react project, check out Tyler McGinnis' series, and take a look at gulpfile.js in your project folder to see how the Sass tasks have been added in.
Once you've installed the dependencies, in your terminal window, run:
gulp
This should start the gulp watch task, which will take care of our jsx transformations and scss processing as we fill out our components. At the end of the project, you can run
gulp production
And all of the files will be minified into a production build. A final note on gulp: If your app does not seem to be responding to changes, check your terminal. If reactify encounters errors in your JSX, it will throw an error and stop watching your files for changes. To resume watching, fix the error in your file(s) and run gulp in your terminal again.
Styles
I've left the complete styles for the project in the project skeleton. You can see the Sass in \sass\app.scss, and the compiled css in \src\css\app.css. In order to complete this article you won't need to modify the styles for the project, but if you wish to make changes to the app.scss file, make sure that you're gulp task is running so that your main stylesheet is updated in response to your changes.
2. The Flux Structure
Before we get into the meat of all of our components, we're going to go over how this project uses Flux and and take a look at how the project's store, dispatcher, and actions are set up.
If you've read articles on react before, you've probably heard of flux, but you may not have used it, as not every react tutorial app is built using flux. Flux is Facebook's application architecture for structuring react applications. You don't have to use flux to make a react application - an application can work just fine without it, or you can use one of the several flux implementations that the community has created, like Reflux or Alt.
So, if an app doesn't need to use flux, then why use it? The flux architecture exists to help make managing data in a react application easier. Without flux, your react components have to take care of the business of managing state and responding to actions to change state all by themselves. While this isn't significant for smaller, simple apps, when your apps start getting larger and involving more pieces, having your state wrapped up in individual components can get hairy quite quickly.
With flux, all of your data is stored in your stores. An application can have many stores, typically one for each discrete area of the app, and your components can listen for changes from several stores. When your components receive change events, they update their state and trigger a re-render in response. Though this may seem elaborate, with react's virtual DOM, this entire process is amazingly fast. And, with flux's dispatcher, as your app increases in complexity, you can update your stores in a specific order depending on the action that occurs.
The main pieces of a flux application are the components, stores, dispatcher, and the actions. Our application has several components, one store, one dispatcher, and a few actions. We also have a file containing our constants, which our store uses to process the different types of actions it receives from the dispatcher..
From the flux docs, his is what the basic flux data flow looks like:
Specific to this app, here's an example of how ours will work:
Store, Dispatcher, Actions
Before we explore the app's components, let's take a look at what makes up the rest of our application. The store is where the state of our application is contained, while the dispatcher and actions are what we use to modify that data in response to events. We'll start with the heart of our application, the store.
Store
Open up stores/calendarStore.js. The store file is relatively long, so let's go piece by piece.
var AppDispatcher = require('../dispatcher/AppDispatcher');
var appConstants = require('../constants/appConstants');
var objectAssign = require('react/lib/Object.assign');
var EventEmitter = require('events').EventEmitter;
var moment = require('moment');
var CHANGE_EVENT = 'change';
The beginning of our store is where we include our actions, constants, object assign, and event emitter. Object assign and event emitter are part of the flux boilerplate code. Together they allow the store to emit change events in response to actions received from components. Our top-level controller view components listen for change events and respond by refreshing their data and re-rendering themselves and their child components.
This portion of our store consists of our event helpers as well as our getter methods. The getter methods obtain the current values in the _store variable. The calendarStore object that these methods exist on is what we will export at the end of our store file, and what all of our other components will be able to access. This ensures that other components can't modify the _store variable on their own - all they can do is retrieve the data in _store, or trigger actions that the store processes using its private setter methods.
var calendarStore = objectAssign({}, EventEmitter.prototype, {
addChangeListener: function(cb) {
this.on(CHANGE_EVENT, cb);
},
removeChangeListener: function(cb) {
this.removeListener(CHANGE_EVENT, cb);
},
getToday: function() {
return _store.today;
},
getDisplayed: function() {
return _store.displayed;
},
getSelected: function() {
return _store.selectedDay;
},
getSearch: function() {
return _store.search;
},
getEvents: function() {
return _store.events;
},
getButton: function() {
return _store.buttonBar;
},
getFilter: function() {
return _store.filter;
}
});
This is one of the features that I like most about organizing a react app with flux. I find it easier to understand the changes to my app's data when every alteration of that data has to be processed through a store. This way, I only have to reference the store files to know exactly how my app's data will change when an action occurs.
Next, we create the _store variable, which is our data model. Store is an object that contains properties for all of the necessary data in our application. Here, we set the initial state of our store. This is what our data will be set to on the initial render of our app.
var _store = {
// today is set when app loads and does not change
today: {
date: moment(),
year: moment().year(),
month: moment().format('MMMM'),
// monthIndex is the month number obtained from moment, incremented by 1 to match node-calendar's format
monthIndex: moment().month() + 1,
weekIndex: moment().week(),
dayIndex: moment().date(),
time: moment().format('h:mm a')
},
// display is the date information that is currently displayed in the calendar view, and is updated via user interaction
// we initially set displayed to match today's date so that calendar can render before API returns data
displayed: {
date: moment(),
year: moment().year(),
month: moment().format('MMMM'),
monthIndex: moment().month() + 1,
weekIndex: moment().week(),
dayIndex: moment().date(),
time: moment().format('h:mm a')
},
// selected is the current user selected day. On initial state it is set to today. Selected day is the day displayed in the task card.
selectedDay: {
date: moment(),
year: moment().year(),
month: moment().format('MMMM'),
monthIndex: moment().month() + 1,
weekIndex: moment().week(),
dayIndex: moment().date(),
},
search: '',
events: [],
// Button bar is the navigation bar on the top of the calendar. Each property controls whether a specific button has an "active" style applied.
buttonBar: {
dayActive: false,
weekActive: false,
monthActive: true,
yearActive: false
},
filter: {
helpShown: true,
needsMetShown: true,
occasionsShown: true
}
}
Next, we have our setter methods. These are the methods that our store will call when the dispatcher sends it a payload and an action type. All that these methods do is modify the values set in the _store variable. In the case of changeSearch and selectDay, the methods simply take the data passed to them and update the store accordingly.
var changeButton = function(data) {
_store.buttonBar = data;
}
var changeSearch = function(data) {
_store.search = data;
};
var changeFilter = function(data) {
_store.filter = data;
};
var selectDay = function(data) {
_store.selectedDay = data;
};
var changeDisplay = function(data) {
_store.displayed = data;
};
var updateEvents = function(data) {
var formattedEvents = data.events.map(function(item) {
return(
{
category: item.category,
content: item.content,
help: item.help,
moment: moment(item.date),
time: moment(item.date).format('h:mm a'),
hour: moment(item.date).format('h a')
}
)
});
_store.events = formattedEvents;
};
For updateEvents, we do a bit of work on the data that is passed using moment.js. When we receive events from our API, they come in a form that is rather raw, so in the method that sets our store's events property we do some work on the data to manipulate each event into a format that our components can make use of later.
Finally, we have our store's callback function and the export statement. With Flux, every store registers a callback function with the app's dispatcher. Typically, the callback is simply a switch statement that processes the data received from the dispatcher.
calendarStore.dispatchToken = AppDispatcher.register(function(action){
switch(action.actionType){
case appConstants.ActionTypes.CHANGE_BUTTON:
changeButton(action.data);
calendarStore.emit(CHANGE_EVENT);
break;
case appConstants.ActionTypes.CHANGE_SEARCH:
changeSearch(action.data);
calendarStore.emit(CHANGE_EVENT);
break;
case appConstants.ActionTypes.SELECT_DAY:
selectDay(action.data);
calendarStore.emit(CHANGE_EVENT);
break;
case appConstants.ActionTypes.RECEIVE_EVENTS:
updateEvents(action.json);
calendarStore.emit(CHANGE_EVENT);
break;
case appConstants.ActionTypes.CHANGE_DISPLAY:
changeDisplay(action.data);
calendarStore.emit(CHANGE_EVENT);
break;
case appConstants.ActionTypes.CHANGE_FILTER:
changeFilter(action.data);
calendarStore.emit(CHANGE_EVENT);
default:
return true;
}
});
module.exports = calendarStore;
First, we declare a variable action and set it equal to the action property on the payload coming from the dispatcher. This property is going to consist of an action type, as well as the data sent to the dispatcher. Then, we have a switch statement that, based on the value of action.actionType, calls one of our setter functions that we defined above, sending along the data from the payload. After that function finishes, we emit a change event to notify our views that the store has been updated.
The last line of our store file is where we export calendarStore to make it available to our other components. Remember, calendarStore is simply our event helpers and our getter methods.
Dispatcher
On to the dispatcher! Open up dispatcher/AppDispatcher.js.
var AppDispatcher = require('flux').Dispatcher;
module.exports = new AppDispatcher();
Our dispatcher is relatively concise, and largely boilerplate. After creating our dispatcher at the top of the file we define its handleAction method. This method will be called by our actions. When called, it takes the data sent in by the action, sets a source (which, in the case of this app is always 'VIEW_ACTION', as none of our actions come from the server) and sets the action property to the data it received. This is what is then dispatched to the callbacks in our stores and processed by the switch statement we just saw in our store. Finally, we export the dispatcher.
Constants
var APIRoot = "https://sheltered-shelf-4779.herokuapp.com";
var appConstants = {
APIEndpoints: {
LOGIN: APIRoot + "/api/v1/login",
EVENTS: APIRoot + "/api/v1/events",
},
ActionTypes: {
CHANGE_SEARCH: "CHANGE_SEARCH",
SELECT_DAY: "SELECT_DAY",
LOGIN_REQUEST: "LOGIN_REQUEST",
LOGIN_RESPONSE: "LOGIN_RESPONSE",
REDIRECT: "REDIRECT",
LOAD_EVENTS: "LOAD_EVENTS",
RECEIVE_EVENTS: "RECEIVE_EVENTS",
LOAD_CAL: "LOAD_CAL",
CHANGE_DISPLAY: "CHANGE_DISPLAY",
CHANGE_BUTTON: "CHANGE_BUTTON",
CHANGE_FILTER: "CHANGE_FILTER"
}
};
module.exports = appConstants;
One more simple file before we take a look at our app's actions. Our constants file, located in constants/appConstants.js, is an object with properties that correspond to our various actions. If you think back to the switch statement in our store, you'll recall that it evaluated the value of action.actionType. The constants in this file are what that value will be. Each constant corresponds to an action.
When I first was building an app using flux, I wondered why we had a constants file that simply had our action keys mirrored as strings. Why not just use strings everywhere? The answer apparently is for minification purposes, as minifiers can substitute a smaller identifier for the constant name, but would have to leave a string as its exact value. Having a constants file also comes in handy if your app grows in size, as you'll be able to tell all of the actions that your app responds to by referencing a single list, rather than multiple stores.
Still, some people object to the use of constants and prefer to do away with them. If you're of the same mind, I suggest checking out the Reflux or Delorean implementations.
Check back soon for more as we take a look at the app's actions and go into each component.