In this post, I'll go through creating a Node app that queries the Contentful API and uses the response data to pre-render a page server-side before sending the HTML off to the client. This is useful since it improves the preceived load time, creating a better User Experience and allows you to use an API to create a page without giving everyone access to your API keys. Our end product will be a webpage for a blog that displays all the blog posts and allows us to filter by category. If the end product isn't something you need, you can still read through to learn a lot about using asynchronous programming with Redux and connecting it to an instance of React. We will use Express to host our server, React to build our components, and Redux to manage our Component state.
Getting Started
To start off, you will need to have Node installed. If you don't, you can find a guide to install it here.
Next, we'll install babel-cli and browserify. To install babel and browserify, input the following commands from the terminal.
npm install --global babel-cli
npm install --global browserify
We'll use babel to compile the jsx files into regular old js files and browserify to compile a js file for the browser by explicitly importing the actual module code from the require('module')
statements into one big file.
Head over to my Github repo for this project and download the package.json file and place it in your project directory. Run npm install
from the terminal and the necessary node dependencies will be automatically installed. You can also view the final files if you would like while you are there.
One last thing you can do, if you want follow along and create the Categories page yourself, is sign-up for a Contentful account. It's free to sign-up and gives you more than enough usage in the free tier to use on your personal site (I use it for this blog). If you do sign-up, choose the Blog example they provide to have a similar data structure to the one I will be using.
Setting Up the Folder Structure
Before we start creating the files for the components, let's set up a place to put everything so we don't just have a mess of files in one directory. The folder structure I'll be using is:
-root
|- pre_compiled_components
|- CategoryDisplay
|- redux
|- contentful
Once you have these directories created, we can start writing our code!
Creating the First Components
The features we want for this categories page are:
- To display all the posts from the blog
- To hide posts from categories of our choosing
- To display the title and excerpt of each blog post
- To link to the actual blog post
We'll start building the components from the bottom-up, beginning with a presentational componenent for an individual post. We want the Post component to display the title, excerpt, and link to the actual post, so we will assume we will have this data available in the props for now (we'll actually pass it in later when we make the parent components).
var Post = (props) => {
<a className="post-link" href={`post/${props.data.url}`}>
<div className='post-title'>
<h3>{props.data.title}</h3>
</div>
<div className='post-excerpt'>
<p>{props.data.summary}</p>
</div>
</a>
}
module.exports = Post;
Now we have a reusable component to display some information about our post that will work anywhere, as long as it receives information about post title, excerpt, and URL.
Next let's make a component that will hold data for each Post component. Keep in mind, that we are separating the Posts by category so this component will need a prop to indicate whether or not the Posts will be visible.
const Post = require('./Post');
class SingleCategoryContainer extends React.Component {
constructor(props) {
super(props);
}
getVisibility() {
return (this.props.visible ? {display: 'inline-block'} : {display: 'none'})
}
render() {
<div className={`${this.props.category}-post-container`} style={this.getVisibility()}>
{this.props.posts.map((e, i) => {
return <Post key={i} data={e} />
})}
</div>
}
}
module.exports = SingleCategoryContainer;
This component receives two props: visible, a boolean indicating whether or not the Category should be displayed, and posts, an array containing data for all the Posts in the Category. The toggle for the visibility is not included in this Component since we would like all the toggles to be placed together. This creates a need for a parent container that will create the SingleCategoryContainers and their corresponding toggles. So let's go ahead and create the component that will hold our categories.
const CategoryToggle = require('./CategoryToggle');
const SingleCategoryContainer = require('./SingleCategoryContainer');
class CategoriesContainer extends React.Component{
constructor(props) {
super(props);
this.toggleCategory = this.toggleCategory.bind(this);
}
toggleCategory(toggle) {
this.props.dispatch(actions.toggleVisibility(toggle));
}
render() {
var categories = [];
for (let category in this.props.categories) {
categories.push(this.props.categories[category]);
}
return (
<div className="article-container">
<div className="title">
<h1>Posts by Category</h1>
</div>
<div className="category-toggles">
{categories.map((e, i) => {
return <CategoryToggle key={e.category} category={e.category} checked={e.visible} click={this.toggleCategory}/>
})}
</div>
<div className="post-grid">
{categories.map((e, i) => {
return <SingleCategoryContainer key={e.category} category={e.category} posts={e.posts} visible={e.visible}/>
})}
</div>
</div>
)
}
}
module.exports = CategoriesContainer;
This component receives the following two props: dispatch, a function that communicates with the Redux state store (I'll talk more about Redux and the state store later, but for now, you just need to know that this is what sends the message when a Category's visibility needs to be toggled), and categories, an object that holds the category name and an array of posts for each category. It creates a CategoryToggle (which we'll make next) and a SingleCategoryContainer (the last component we made) for every category.
In the render function, we turn the object of categories into an array of categories where each index of the array holds an object containing the category name and an array of posts. This is done so we can use map on the array to make our lives a little easier when we make the React HTML in the return statement.
Now let's make the component to toggle the visibility for every category.
class CategoryToggle extends React.Component {
constructor(props) {
super(props);
this.clicked = this.clicked.bind(this);
}
clicked() {
this.props.click(this.props.category);
}
render() {
return (
<div className="checkbox">
<input type="checkbox" id={`${this.props.category}-checkbox`} onClick={this.clicked} defaultChecked={this.props.checked}></input>
<label htmlFor={`${this.props.category}-checkbox`}>{this.props.category}</label>
</div>
)
}
}
module.exports = CategoryToggle;
Like we saw in the CategoriesContainer.jsx file, CategoryToggle component receives three props: category, a string which lets the Toggle which category it is in charge of, checked, a boolean indicating whether or not the CategoryToggle is checked, and click, a function that sends a message to indicate which SingleCategoryContainer's visibility should be toggled. It creates a checkbox that indicates the current visibility state of each category and sends a message, when clicked, to toggle the visibility of a certain category.
All we have left to do now is make the MainContainer component that will coordinate state with Redux and send it down to the Presentational components we just created.
const React = require('react');
const react_redux = require('react-redux');
const CategoriesContainer = require('./CategoriesContainer');
const mapStateToProps = (state) => {
return {
categories: state
};
}
let MainContainer = react_redux.connect(
mapStateToProps
)(CategoriesContainer);
module.exports = MainContainer;
This is a pretty simple component since most of the work for it will be done in the reducer file, which we'll make next. Basically, what's happening here is that we are use MainContainer as a bridge between the Redux state tree and the CategoriesContainer object. The Redux state tree for this project, as we'll see soon, contains an object for each category that has information about the posts in that category, the visibility state, and the category name. This information is the same information that we saw being passed through our Components above.
Now give yourself a pat on the back because we just got through a sizable portion of the work! All we have to do now is create the Redux store and the set-up the Express app.
Creating the Redux Store
The whole point behind Redux is that it tries to streamline the state of a web application by making state changes predictable. It does this by only allowing state changes when an action is dispatched (remember the dispatch function from the CategoriesContainer.jsx file). This way all changes are piped through one place and updates are sent afterwards, so you will be able to track what's changing what easier. Now that you have a super simplified idea about what Redux does, let's make the reducer file that controls the state updates. Since this is a large file, I'm going to split it up into its components (which I should have done to start with but this project is small enough that I left everything in one file).
const contentful = require('../../../contentful/contentfulAPI');
//Actions
const ADD_CATEGORY = 'ADD_CATEGORY';
const TOGGLE_VISIBILITY = 'TOGGLE_VISIBILITY';
const ADD_POST = 'ADD_POST';
const addCategory = category => {
return { type: ADD_CATEGORY, category };
};
const toggleVisibility = category => {
return { type: TOGGLE_VISIBILITY, category };
};
const addPost = (category, data) => {
return { type: ADD_POST, category, data };
};
The above portion of the file sets up the actions that can happen during the life of the application. Each of these actions can be used as a parameter for the dispatch function, which lets Redux know what portion of the state tree should be updated and the new data it should be updated with. This is the crux of how Redux makes state changes predictable since you have to pass one of these actions to the dispatch functions to create a change.
Next we'll create the portion of the file that handles the data received with the actions from above.
//Action Handlers
const handleAction = (state = {}, action) => {
switch (action.type) {
case ADD_CATEGORY:
return Object.assign({}, state, newCategory(state[action.category], action));
case TOGGLE_VISIBILITY:
return Object.assign({}, state, {[action.category]: manageVisibility(state[action.category], action)});
case ADD_POST:
return Object.assign({}, state, {[action.category]: newPost(state[action.category], action)});
default:
return state;
}
};
const newCategory = (state = {}, action) => {
return {
\[action.category]: {
posts: [],
visible: true,
category: action.category
}}
};
const manageVisibility = (state = {}, action) => {
return Object.assign({}, state, {
visible: !state.visible,
});
};
const newPost = (state = {}, action) => {
return Object.assign({}, state, {
posts: [...state.posts, ]
});
};
This portion looks a lot more complicated than it actually is. All that is happening is the handleAction function reads what action was sent, then it calls the corresponding function to update the state. We are using Object.assign
to create the new state file because we don't want to alter the old state object. This won't be of use to us in this project, but if you are working on a project that needs to keep a history of previous states, such as an undo mechanism for a text editor, keeping the old state untouched is vital.
How Object.assign
works is as follows. The first parameter is the starting object, in our case an empty object. The second parameter is the object we want to create a copy of, and the last parameter is the portion of the object we want to provide new data to.
The last portion of the file is what queries Contentful for content. I'll be leaving the Contentful API as a black-box, but you can look through their website for documentation since they explain it pretty well.
const fetchCategories = () => {
return dispatch => {
return contentful.client.getEntries({
content_type: contentful.typeID.category
}).then(data => {
var categories = [];
for (var i = 0; i < data.items.length; i++) {
categories.push(data.items[i].fields.title);
dispatch(addCategory(data.items[i].fields.title));
}
return categories;
});
};
};
function fetchPosts(categories, dispatch) {
return dispatch => {
let promises = categories.map(function(category) {
return new Promise((resolve, reject) => {
contentful.client.getEntries({
content_type: contentful.typeID.post,
'fields.tags[ne]': category
}).then(data => {
for (let i = 0; i < data.items.length; i++) {
dispatch(addPost(category, contentful.extractPostInfo(data.items[i])));
}
resolve();
})
})
})
return Promise.all(promises).then(val => {
return val;
});
}
}
module.exports = {
addCategory,
toggleVisibility,
addPost,
handleAction,
fetchCategories,
fetchPosts
};
The fetchCategories function is pretty straight-forward. It queries Contentful for all the categories present in our content model, dispatches 'addCategory' event to make Redux update our state, and returns an array of all the categories it found.
The fetchPosts function is a little more complicated because of the promises so I'll go more in depth. It takes the array of categories from fetchCategories (we'll see how the array is passed to it later) and creates a promise for each category that will query Contentful for all the posts in that category. Each promise individually dispatches an 'addPost' event as it's going through the query results to update the Redux state with the information for every post. After a promise has gone through all the posts it found, it resolves. We return the array of promises to our calling function, which will only proceed when all the promises have finished processing the post information.
In the end, our Redux state tree will have the following structure:
{
category1: {
visible: true,
posts: [],
category: category1
},
category2: {
visible: true,
posts: [],
category: category2
}
}
I hope you haven't passed out from all the work we've done, because we only have one more major thing to do in terms of writing code, which is to set-up our Express server!
The Server for Server-Side Rendering
Compared to what we just did, this next part should be a walk in the park, so I'll get right into it.
const MainContainer = require('./components/CategoryDisplay/MainContainer');
const actions = require('./components/redux/reducers/actions');
const reactDOM = require('react-dom/server');
const React = require('react');
const redux = require('redux');
const Provider = require('react-redux').Provider;
const thunk = require('redux-thunk').default;
const express = require('express');
const handleRender = (req, res) => {
const store = redux.createStore(actions.handleAction, redux.applyMiddleware(thunk));
store.dispatch(actions.fetchCategories())
.then(categories => store.dispatch(actions.fetchPosts(categories)))
.then(() => {
const html = reactDOM.renderToString(
<Provider store={store} >
<MainCategoryContainer />
</Provider>
);
const preloadedState = store.getState();
res.send(renderFullPage(html,domString));
res.end();
});
};
function renderFullPage(html, preloadedState) {
return `
<html>
<head>
<title>Category Page Example</title>
</head>
<body>
<div id="react-container">${html}</div>
<script>
window.\_\_PRELOADED_STATE\___ = ${JSON.stringify(preloadedState).replace(/</g, '\\\u003c')}
</script>
<script src="components/bundle.js"></script>
</body>
</html>
`;
}
const app = express();
const port = 8000;
app.use(express.static('components'));
app.use(handleRender);
app.listen(port);
This file shouldn't be too difficult to understand if you remember the functions we wrote earlier. Pretty much all of the work is being done in the handleRender function so let's go through that. The first thing we do is create the Redux store using `redux.createStore`, which will be managing our state. We pass it the handleAction function from actions.jsx and apply the 'thunk' middleware. The middleware is essential to asynchronous Redux programming because it allows us to use dispatch with Promises, which you'll see the importance of soon (if you want a more detailed explanation about middleware and how they work, you go [here](http://redux.js.org/docs/advanced/Middleware.html)).
Next, we dispatch the fetchCategories function, which returns an array of categories. When fetchCategories completes, the categories are passed to fetchPosts. At this point, the app waits for the Promises from fetchPosts to resolve before moving to the next portion. Once the Promises resolve, our state tree should be fully built and ready to rendered into HTML. That is exactly what we do with the `ReactDOM.renderToString` call. The 'Provider' component we are wrapping MainContainer passes the 'store' object as a prop to every component in MainContainer so we don't have to do it ourselves. Now, we get the current state tree from our Redux store, save it as an object, and create the HTML, with the state embed inside 'window.\_\_PRELOADEDSTATE\_\_, that we will send to the client.
If you'll notice inside the HTML, there's mention of a bundle.js file, which is the last thing we need to create, but don't worry! All the work for that file has been done already and we just need to put it together!
### Compiling the Files
Let's create the bundle.jsx file, which just includes the React and Redux files that the client will need to parse the app on their end.
const MainContainer = require('./components/CategoryDisplay/MainContainer');
const actions = require('./components/redux/reducers/actions');
const reactDOM = require('react-dom/server');
const React = require('react');
const redux = require('redux');
const Provider = require('react-redux').Provider;
const preloadedState = window.__PRELOADED_STATE__;
delete window.__PRELOADED_STATE__;
const store = redux.createStore(action, preloadedState);
reactDOM.render(
<Provider store={store} >
<MainCategoryContainer />
</Provider,
document.getElementById('react-container')
)
There isn't any new code in this file. All that we are doing is loading the state from window.__PRELOADED_STATE__ that we saved server-side and then creating the React components using that state.
The one last thing we have to do is compile the jsx files into js files and create a bundle.js that can run on the browser (since browsers don't support the 'require' method)
This part is really easy! All that we need to do is run a few commands from the terminal using babel-cli and browserify that we installed earlier. First cd
into the pre_compiled_components directory and run the following command
babel ./ -d ../components
This tells babel to find all the jsx files in the current directory (the ./ part), compile them, and then place the compiled files into the components directory (-d means output to the following directory). Next, cd
into the newly created components directory and run
browserify client.js -o bundle.js
This tells browserify to find the client.js file, import all its dependencies into one large js file, and then output (-o) with the name given.
Now copy the PageRenderer.js file into the root of your project directory, run it using node PageRenderer.js
from the terminal, and your app should be working!
Closing Statements
If you made it this far congratulations! This process was extremely difficult for me to understand when I was learning it, and I probably didn't explain some of the steps as well as I should have. If you are stuck on a certain portion, hit me up on Twitter with any questions and I'll be glad to help out. You can also go to the Redux site, which has plenty of documentation to get you through the portion you may be stuck on.
Social