- Break It Down for Me
- The Store: the Single Source of Truth
- React Components: Higher and Lower Level
- Page Components
- AppDispatcher
- Actions: Last Stop Before the Store
- Configure Your Cosmic JS CMS
- Server-side Rendering
- Conclusion
- Frequently Asked Questions about Building a React Universal Blog App and Implementing Flux
In the first part of this miniseries, we started digging into the world of React to see how we could use it, together with Node.js, to build a React Universal Blog App.
In this second and last part, we’ll take our blog to the next level by learning how to add and edit content. We’ll also get into the real meat of how to easily scale our React Universal Blog App using React organizational concepts and the Flux pattern.
Break It Down for Me
As we add more pages and content to our blog, our routes.js
file will quickly become big. Since it’s one of React’s guiding principles to break things up into smaller, manageable pieces, let’s separate our routes into different files.
Open your routes.js
file and edit it so that it’ll have the following code:
// routes.js
import React from 'react'
import { Route, IndexRoute } from 'react-router'
// Store
import AppStore from './stores/AppStore'
// Main component
import App from './components/App'
// Pages
import Blog from './components/Pages/Blog'
import Default from './components/Pages/Default'
import Work from './components/Pages/Work'
import NoMatch from './components/Pages/NoMatch'
export default (
<Route path="/" data={AppStore.data} component={App}>
<IndexRoute component={Blog}/>
<Route path="about" component={Default}/>
<Route path="contact" component={Default}/>
<Route path="work" component={Work}/>
<Route path="/work/:slug" component={Work}/>
<Route path="/blog/:slug" component={Blog}/>
<Route path="*" component={NoMatch}/>
</Route>
)
We’ve added a few different pages to our blog and significantly reduced the size of our routes.js
file by breaking the pages up into separate components. Moreover, note that we’ve added a Store by including AppStore
, which is very important for the next steps in scaling out our React application.
The Store: the Single Source of Truth
In the Flux pattern, the Store is a very important piece, because it acts as the single source of truth for data management. This is a crucial concept in understanding how React development works, and one of the most touted benefits of React. The beauty of this discipline is that, at any given state of our app we can access the AppStore
‘s data and know exactly what’s going on within it. There are a few key things to keep in mind if you want to build a data-driven React application:
- We never manipulate the DOM directly.
- Our UI answers to data and data live in the store
- If we need to change out our UI, we can go to the store and the store will create the new data state of our app.
- New data is fed to higher-level components, then passed down to the lower-level components through
props
composing the new UI, based on the new data received.
With those four points, we basically have the foundation for a one-way data flow application. This also means that, at any state in our application, we can console.log(AppStore.data)
, and if we build our app correctly, we’ll know exactly what we can expect to see. You’ll experience how powerful this is for debugging as well.
Now let’s create a store folder called stores
. Inside it, create a file called AppStore.js
with the following content:
// AppStore.js
import { EventEmitter } from 'events'
import _ from 'lodash'
export default _.extend({}, EventEmitter.prototype, {
// Initial data
data: {
ready: false,
globals: {},
pages: [],
item_num: 5
},
// Emit change event
emitChange: function(){
this.emit('change')
},
// Add change listener
addChangeListener: function(callback){
this.on('change', callback)
},
// Remove change listener
removeChangeListener: function(callback) {
this.removeListener('change', callback)
}
})
You can see that we’ve attached an event emitter. This allows us to edit data in our store, then re-render our application using AppStore.emitChange()
. This is a powerful tool that should only be used in certain places in our application. Otherwise, it can be hard to understand where AppStore
data is being altered, which brings us to the next point…
React Components: Higher and Lower Level
Dan Abramov wrote a great post on the concept of smart and dumb components. The idea is to keep data-altering actions just in the higher-level (smart) components, while the lower-level (dumb) components take the data they’re given through props and render UI based on that data. Any time there’s an action performed on a lower-level component, that event is passed up through props to the higher-level components in order to be processed into an action. Then it redistributes the data (one-way data flow) back through the application.
Said that, let’s start building some components. To do that, create a folder called components
. Inside it, create a file called App.js
with this content:
// App.js
import React, { Component } from 'react'
// Dispatcher
import AppDispatcher from '../dispatcher/AppDispatcher'
// Store
import AppStore from '../stores/AppStore'
// Components
import Nav from './Partials/Nav'
import Footer from './Partials/Footer'
import Loading from './Partials/Loading'
export default class App extends Component {
// Add change listeners to stores
componentDidMount(){
AppStore.addChangeListener(this._onChange.bind(this))
}
// Remove change listeners from stores
componentWillUnmount(){
AppStore.removeChangeListener(this._onChange.bind(this))
}
_onChange(){
this.setState(AppStore)
}
getStore(){
AppDispatcher.dispatch({
action: 'get-app-store'
})
}
render(){
const data = AppStore.data
// Show loading for browser
if(!data.ready){
document.body.className = ''
this.getStore()
let style = {
marginTop: 120
}
return (
<div className="container text-center" style={ style }>
<Loading />
</div>
)
}
// Server first
const Routes = React.cloneElement(this.props.children, { data: data })
return (
<div>
<Nav data={ data }/>
{ Routes }
<Footer data={ data }/>
</div>
)
}
}
In our App.js
component, we’ve attached an event listener to our AppStore
that will re-render the state when AppStore
emits an onChange
event. This re-rendered data will then be passed down as props to the child components. Also note that we’ve added a getStore
method that will dispatch the get-app-store
action to render our data on the client side. Once the data has been fetched from the Cosmic JS API, it will trigger an AppStore
change that will include AppStore.data.ready
set to true
, remove the loading sign and render our content.
Page Components
To build the first page of our blog, create a Pages
folder. Inside it, we’ll create a file called Blog.js
with the following code:
// Blog.js
import React, { Component } from 'react'
import _ from 'lodash'
import config from '../../config'
// Components
import Header from '../Partials/Header'
import BlogList from '../Partials/BlogList'
import BlogSingle from '../Partials/BlogSingle'
// Dispatcher
import AppDispatcher from '../../dispatcher/AppDispatcher'
export default class Blog extends Component {
componentWillMount(){
this.getPageData()
}
componentDidMount(){
const data = this.props.data
document.title = config.site.title + ' | ' + data.page.title
}
getPageData(){
AppDispatcher.dispatch({
action: 'get-page-data',
page_slug: 'blog',
post_slug: this.props.params.slug
})
}
getMoreArticles(){
AppDispatcher.dispatch({
action: 'get-more-items'
})
}
render(){
const data = this.props.data
const globals = data.globals
const pages = data.pages
let main_content
if(!this.props.params.slug){
main_content = <BlogList getMoreArticles={ this.getMoreArticles } data={ data }/>
} else {
const articles = data.articles
// Get current page slug
const slug = this.props.params.slug
const articles_object = _.keyBy(articles, 'slug')
const article = articles_object[slug]
main_content = <BlogSingle article={ article } />
}
return (
<div>
<Header data={ data }/>
<div id="main-content" className="container">
<div className="row">
<div className="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
{ main_content }
</div>
</div>
</div>
</div>
)
}
}
This page is going to serve as a template for our blog list page (home) and our single blog pages. Here we’ve added a method to our component that will get the page data prior to the component mounting using the React lifecycle componentWillMount
method. Then, once the component has mounted at componentDidMount()
, we’ll add the page title to the <title>
tag of the document.
Along with some of the rendering logic in this higher-level component, we’ve included the getMoreArticles
method. This is a good example of a call to action that’s stored in a higher-level component and made available to lower-level components through props.
Let’s now get into our BlogList
component to see how this works.
Create a new folder called Partials
. Then, inside it, create a file called BlogList.js
with the following content:
// BlogList.js
import React, { Component } from 'react'
import _ from 'lodash'
import { Link } from 'react-router'
export default class BlogList extends Component {
scrollTop(){
$('html, body').animate({
scrollTop: $("#main-content").offset().top
}, 500)
}
render(){
let data = this.props.data
let item_num = data.item_num
let articles = data.articles
let load_more
let show_more_text = 'Show More Articles'
if(data.loading){
show_more_text = 'Loading...'
}
if(articles && item_num <= articles.length){
load_more = (
<div>
<button className="btn btn-default center-block" onClick={ this.props.getMoreArticles.bind(this) }>
{ show_more_text }
</button>
</div>
)
}
articles = _.take(articles, item_num)
let articles_html = articles.map(( article ) => {
let date_obj = new Date(article.created)
let created = (date_obj.getMonth()+1) + '/' + date_obj.getDate() + '/' + date_obj.getFullYear()
return (
<div key={ 'key-' + article.slug }>
<div className="post-preview">
<h2 className="post-title pointer">
<Link to={ '/blog/' + article.slug } onClick={ this.scrollTop }>{ article.title }</Link>
</h2>
<p className="post-meta">Posted by <a href="https://cosmicjs.com" target="_blank">Cosmic JS</a> on { created }</p>
</div>
<hr/>
</div>
)
})
return (
<div>
<div>{ articles_html }</div>
{ load_more }
</div>
)
}
}
In our BlogList
component, we’ve added an onClick
event to our Show More Articles
button. The latter executes the getMoreArticles
method that was passed down as props from the higher-level page component. When that button is clicked, the event bubbles up to the Blog
component and then triggers an action on the AppDispatcher
. AppDispatcher
acts as the middleman between our higher-level components and our AppStore
.
For the sake of brevity, we’re not going to build out all of the Page
and Partial
components in this tutorial, so please download the GitHub repo and add them from the components
folder.
AppDispatcher
The AppDispatcher
is the operator in our application that accepts information from the higher-level components and distributes actions to the store, which then re-renders our application data.
To continue this tutorial, create a folder named dispatcher
. Inside it, create a file called AppDispatcher.js
, containing the following code:
// AppDispatcher.js
import { Dispatcher } from 'flux'
import { getStore, getPageData, getMoreItems } from '../actions/actions'
const AppDispatcher = new Dispatcher()
// Register callback with AppDispatcher
AppDispatcher.register((payload) => {
let action = payload.action
switch(action) {
case 'get-app-store':
getStore()
break
case 'get-page-data':
getPageData(payload.page_slug, payload.post_slug)
break
case 'get-more-items':
getMoreItems()
break
default:
return true
}
return true
})
export default AppDispatcher
We’ve introduced the Flux
module into this file to build our dispatcher. Let’s add our actions now.
Actions: Last Stop Before the Store
To start, let’s create an actions.js
file inside a newly created folder called actions
. This file will feature the following content:
// actions.js
import config from '../config'
import Cosmic from 'cosmicjs'
import _ from 'lodash'
// AppStore
import AppStore from '../stores/AppStore'
export function getStore(callback){
let pages = {}
Cosmic.getObjects(config, function(err, response){
let objects = response.objects
/* Globals
======================== */
let globals = AppStore.data.globals
globals.text = response.object['text']
let metafields = globals.text.metafields
let menu_title = _.find(metafields, { key: 'menu-title' })
globals.text.menu_title = menu_title.value
let footer_text = _.find(metafields, { key: 'footer-text' })
globals.text.footer_text = footer_text.value
let site_title = _.find(metafields, { key: 'site-title' })
globals.text.site_title = site_title.value
// Social
globals.social = response.object['social']
metafields = globals.social.metafields
let twitter = _.find(metafields, { key: 'twitter' })
globals.social.twitter = twitter.value
let facebook = _.find(metafields, { key: 'facebook' })
globals.social.facebook = facebook.value
let github = _.find(metafields, { key: 'github' })
globals.social.github = github.value
// Nav
const nav_items = response.object['nav'].metafields
globals.nav_items = nav_items
AppStore.data.globals = globals
/* Pages
======================== */
let pages = objects.type.page
AppStore.data.pages = pages
/* Articles
======================== */
let articles = objects.type['post']
articles = _.sortBy(articles, 'order')
AppStore.data.articles = articles
/* Work Items
======================== */
let work_items = objects.type['work']
work_items = _.sortBy(work_items, 'order')
AppStore.data.work_items = work_items
// Emit change
AppStore.data.ready = true
AppStore.emitChange()
// Trigger callback (from server)
if(callback){
callback(false, AppStore)
}
})
}
export function getPageData(page_slug, post_slug){
if(!page_slug || page_slug === 'blog')
page_slug = 'home'
// Get page info
const data = AppStore.data
const pages = data.pages
const page = _.find(pages, { slug: page_slug })
const metafields = page.metafields
if(metafields){
const hero = _.find(metafields, { key: 'hero' })
page.hero = config.bucket.media_url + '/' + hero.value
const headline = _.find(metafields, { key: 'headline' })
page.headline = headline.value
const subheadline = _.find(metafields, { key: 'subheadline' })
page.subheadline = subheadline.value
}
if(post_slug){
if(page_slug === 'home'){
const articles = data.articles
const article = _.find(articles, { slug: post_slug })
page.title = article.title
}
if(page_slug === 'work'){
const work_items = data.work_items
const work_item = _.find(work_items, { slug: post_slug })
page.title = work_item.title
}
}
AppStore.data.page = page
AppStore.emitChange()
}
export function getMoreItems(){
AppStore.data.loading = true
AppStore.emitChange()
setTimeout(function(){
let item_num = AppStore.data.item_num
let more_item_num = item_num + 5
AppStore.data.item_num = more_item_num
AppStore.data.loading = false
AppStore.emitChange()
}, 300)
}
There are a few methods here that are exposed by this actions.js
file. getStore()
connects to the Cosmic JS API to serve our blog’s content. getPageData()
gets the page data from a provided slug
(or page key). getMoreItems()
controls how many items will be seen in our BlogList
and WorkList
components.
When getMoreItems()
is triggered, it first sets AppStore.data.loading
to true
. Then, 300 milliseconds later (for effect), it allows five more items to be added to our list of blog posts or work items. Finally, it sets AppStore.data.loading
to false
.
Configure Your Cosmic JS CMS
To begin receiving data from your cloud-hosted content API on Cosmic JS, let’s create a config.js
file. Open this file and paste the following content:
// config.js
export default {
site: {
title: 'React Universal Blog'
},
bucket: {
slug: process.env.COSMIC_BUCKET || 'react-universal-blog',
media_url: 'https://cosmicjs.com/uploads',
read_key: process.env.COSMIC_READ_KEY || '',
write_key: process.env.COSMIC_WRITE_KEY || ''
},
}
This means content will be coming from the Cosmic JS bucket react-universal-blog
. To create content for your own blog or app, sign up for a free account with Cosmic JS. When asked to “Add a New Bucket”, click “Install Starter Bucket” and you’ll be able to follow the steps to install the “React Universal Blog”. Once this is done, you can add your unique bucket’s slug to this config file.
Server-side Rendering
Now that we have most of our React components and Flux architecture set up, let’s finish up by editing our app-server.js
file to render everything in server-side production. This file will have the following code:
// app-server.js
import React from 'react'
import { match, RoutingContext, Route, IndexRoute } from 'react-router'
import ReactDOMServer from 'react-dom/server'
import express from 'express'
import hogan from 'hogan-express'
import config from './config'
// Actions
import { getStore, getPageData } from './actions/actions'
// Routes
import routes from './routes'
// Express
const app = express()
app.engine('html', hogan)
app.set('views', __dirname + '/views')
app.use('/', express.static(__dirname + '/public/'))
app.set('port', (process.env.PORT || 3000))
app.get('*',(req, res) => {
getStore(function(err, AppStore){
if(err){
return res.status(500).end('error')
}
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
// Get page data for template
const slug_arr = req.url.split('/')
let page_slug = slug_arr[1]
let post_slug
if(page_slug === 'blog' || page_slug === 'work')
post_slug = slug_arr[2]
getPageData(page_slug, post_slug)
const page = AppStore.data.page
res.locals.page = page
res.locals.site = config.site
// Get React markup
const reactMarkup = ReactDOMServer.renderToStaticMarkup(<RoutingContext {...renderProps} />)
res.locals.reactMarkup = reactMarkup
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// Success!
res.status(200).render('index.html')
} else {
res.status(404).render('index.html')
}
})
})
})
app.listen(app.get('port'))
console.info('==> Server is listening in ' + process.env.NODE_ENV + ' mode')
console.info('==> Go to http://localhost:%s', app.get('port'))
This file uses our getStore
action method to get our content from the Cosmic JS API server-side, then goes through React Router to determine which component will be mounted. Everything will then be rendered into static markup with renderToStaticMarkup
. This output is then stored in a template variable to be used by our views/index.html
file.
Once again, let’s update the scripts
section of our package.json
file so that it looks like the one shown below:
"scripts": {
"start": "npm run production",
"production": "rm -rf public/index.html && NODE_ENV=production webpack -p && NODE_ENV=production babel-node app-server.js --presets es2015",
"webpack-dev-server": "NODE_ENV=development PORT=8080 webpack-dev-server --content-base public/ --hot --inline --devtool inline-source-map --history-api-fallback",
"development": "cp views/index.html public/index.html && NODE_ENV=development webpack && npm run webpack-dev-server"
},
We can now run in development mode with hot reloading and we can run in production mode with server-rendered markup. Run the following command to run the full React Universal Blog Application in production mode:
npm start
Our blog is now ready to view at http://localhost:3000. It can be viewed on the server side, the browser side, and our content can be managed through Cosmic JS, our cloud-hosted content platform.
Conclusion
React is a very sophisticated way to manage UI and data within an application. It’s also a very good choice for rendering server-side content, to appease JavaScript-depraved web crawlers and for rendering UI browser-side to keep us browsing fast. And we can get the best results of both worlds by making our application universal.
I really hope you enjoyed this article. Once again, the full code can be downloaded from GitHub.
Frequently Asked Questions about Building a React Universal Blog App and Implementing Flux
What is the Flux architecture and why is it important in React?
Flux is an architectural pattern that enforces unidirectional data flow — its core objective is to control derived data and enable communication between multiple components using a central Store which holds all state. In the context of React, Flux is extremely important because it allows for predictable state management, making the code easier to maintain.
How does Flux differ from Redux?
While both Flux and Redux are patterns for managing state in JavaScript applications, they have some key differences. Flux has multiple stores each with different parts of the state, while Redux has a single store with hierarchical reducers. Flux has explicit actions that are dispatched and cause state updates, while Redux has implicit actions defined by the return values of reducer functions.
How do I implement Flux in a React application?
Implementing Flux in a React application involves several steps. First, you need to create the Actions which are helper methods that facilitate passing data to the Dispatcher. Next, you create the Dispatcher itself. Then, you create the Store which will hold and manage the application state. Finally, you create the Views (React components) which will react to changes in the Store.
What are the benefits of using a universal blog app?
A universal blog app, also known as an isomorphic app, is an application that can run both on the client-side and the server-side. This has several benefits including improved performance, better SEO, and a more consistent user experience.
How do I handle asynchronous actions in Flux?
Asynchronous actions in Flux can be handled using the dispatcher. When an asynchronous action is initiated, an action is dispatched to signal this. When the asynchronous operation completes, another action is dispatched containing the result of the operation.
How does Flux help in building large scale applications?
Flux is particularly useful in large scale applications because it provides a clear and predictable pattern for managing state. This makes the application easier to understand and maintain, especially as it grows in complexity.
Can I use Flux with other libraries or frameworks?
Yes, while Flux was designed to be used with React, it can be used with any library or framework that can interact with JavaScript objects.
What are the main components of Flux architecture?
The main components of Flux architecture are the Dispatcher, Stores, and Views (React components). Actions are also a key part of Flux, but they are not considered a part of the core architecture.
How does data flow in a Flux application?
In a Flux application, data flows in a single direction. Actions are dispatched to the Dispatcher, which then updates the Stores. The Stores then emit a change event, causing the Views to re-render.
How do I test a Flux application?
Testing a Flux application typically involves unit testing the individual parts of the Flux architecture (Actions, Store, and Views) and then integration testing the application as a whole.
Tony Spiro is a software engineer specializing in front end and back end JavaScript using React and Node.js. He is also the Co-Founder and CEO of Cosmic JS. In his free time you can find him playing music, binging on movies and TV shows and hanging out at home with his wife and dog.