Building a blogging platform Using React, GraphQL, And GraphCMS
In this article, I am going to explore using GraphCMS, React.js, and GraphQL to develop a prototype CRUD (Create, Read, Update, Delete) application by leveraging the Content and Mutation APIS — the two API categories exposed by the GraphCMS software.
Traditionally, content management systems have been bulky, all-in-one software solutions for effectively creating, managing, storing, and retrieving content.
Although these large, unwieldy software were the perfect solution to the problems (content creation, management, and persistence) that they were created to solve, modern problems require modern solutions, and this is why the concept of Headless content management systems evolved.
Examples of traditional content management systems include WordPress, Drupal, and Joomla.
Headless content management systems are a new breed of content management tools that, unlike their more traditional predecessors, are developed around the core idealogy that data (content) should be created, managed, and stored as a standalone component, separate from presentation logic.
This means that content can be created, updated, and stored as a separate entity from the presentation product, therefore allowing the content management process to be lightweight and streamlined.
The core idea behind modern headless CMS tools is that content should be a separate component and must be persisted in a way that allows it to be easily injected in, or detached from, presentation logic.
It is easy to see the many advantages that this approach to content development offers. Creating content using the WordPress platform, for example, means that the content will only be available on the WordPress platform. This precludes the freedom to switch the choice content management system to some other platform: Joomla, for example.
On the other hand, headless content management systems allow creators to easily and seamlessly switch the logic behind their presentation (React.js, for example) to a different technology or platform (Angular, for instance) without having t worry about data malformation or data loss.
While there are many great products built around the headless CMS idea, GraphCMS is an emerging product that has really come to stay. It offers several features for managing content and these have made it a fast-rising star in the industry.
About The Technology
The core technologies I’ll be using are React.js, GraphQL, and GraphCMS.
React.js is a lightweight client-side library that is great for developing awesome user interfaces for the web, mobile, and desktop platforms using JavaScript. Released in 2013, React is arguably the most popular clientside tool for creating user interfaces, especially for the web software platform.
The beauty of React is that it encourages software to be built using a component-based methodology.
React apps are usually made up of components that can be reused in multiple instances throughout the codebase. This approach ensures uniformity throughout the software development pipeline.
Read more about React.js here: https://reactjs.org.
GraphQL is a Query Language for storing and querying data. It is a powerful alternative to traditional protocols such as the popular REST API protocol. GraphQL allows the client to request ONLY the data it needs from the server. This makes it highly efficient at syncing data between the client and the server.
Read more about GraphQL here: https://graphql.org.
GraphCMS is a powerful headless CMS offering that uses GraphQL technology for querying data and performing mutations (or updates) to the content. Common use cases where it makes sense to choose GraphQL + GraphCMS over traditional content management systems are:
- Blogging platforms and blogging software
- Online stores and e-commerce
- Customer Relationship Management (CRM) software
- Productivity software
- Simple websites, web apps and mobile apps.
In this article, I will develop a prototype blogging platform with a front-facing webpage for viewing and interacting with posts.
At the end of the article, the software will allow users to create and publish posts and view published posts.
Getting Started With GraphCMS
The onboarding process for GraphCMS is quite simple and user-friendly.
First, you’ll need to create an account with GraphCMS if you don’t have one already:
- Go to https://app.graphcms.com/signup to create a free-forever account.
- After the account creation process, log in to your dashboard and click Create a new project from scratch.
- Enter a name and short description for your project. A good name might be “Blog CMS Using React, GraphQL and GraphCMS”.
- Select a region on the map to have your content served from and click Create Project. For optimal results loading your content, choose the region that is nearest your location (or the likely location of your audience).
- Select the Free Forever plan to proceed. Of course, you can choose a paid plan later if you need it.
- Click Invite Later to continue. GraphCMS offers an exciting collaboration feature that is handy when working with a team on a project, but we won’t be using this now. You should be presented with a screen that looks like this:
In the sidebar menu, click Schema (the third option) to continue.
The Schema page allows us to layout out our content and add/customize fields for our content.
In the Models tab, click Add.
- Enter post in the Display Name field.
- Enter a short description of the model.
- Click Create Model to create your post model.
- Toggle Show system fields on.
We need to create extra fields (title, author, body, coverImage and trending) for our model.
Title: A short title for each post.
Author: The author of each post.
Body: The content of each post.
coverImage: A cover image for each post.
Trending: A boolean indicating whether a post is currently trending among readers or not.
The sidebar to the right provides different field types for the different data we need to store about our posts:
- Click Single line text for the Title field.
- Enter the Display name: title.
- Enter the API ID: title.
- Enter a short Description.
- In the Validations tab, select Mark field required.
- Click Create.
Repeat these steps for the author field.
For the body field:
- Click Multi line text for the Body field.
- Enter the Display name: body.
- Enter the API ID: body.
- Enter a short Description.
- In the Validations tab, select Mark field required.
- Click Create.
For the Trending field:
- Click Boolean.
- Enter the Display name: trending.
- Enter the API ID: trending.
- Enter a short Description.
- In the Advanced tab, select Set initial value and set the initial value to False.
- Click Create.
For the coverImage field:
- Click Asset picker.
- Enter the Display name: coverImage.
- Enter the API ID: coverImage.
- Enter a short Description.
- Click Create.
Next, click Content in the sidebar menu (fourth option). Here, we’ll enter some data for our model.
Click Create Item. Enter some dummy data for the new entry and click Save and Publish.
Do this a couple more times. Two or three posts should be enough to work with.
Next, click Settings in the sidebar menu and make a few changes:
Click Save.
Next, create a new access token and save it.
At this point, you are ready to begin developing the client app.
Developing The Client App
We’ll need an audience-facing app as a proof of concept. Basically, we need a blog so that readers of our content will be able to view (and engage with) posts that we publish. Fortunately, setting up a functional prototype isn’t difficult at all. We’ll use Create React App, the popular CLI tool, to scaffold a new React application.
Note: Throughout this tutorial, I will use NPM as my package manager of choice. If you’d prefer to use Yarn instead, you can still follow along by replacing NPM commands with the corresponsing Yarn ones.
- Ensure you have both Node.js and the Node Package Manager (NPM) installed by starting a terminal instance and running:
node -v && npm -v
If both Node.js and NPM are installed, you should still the version numbers for both Node.js and NPM printed thus:
If you do not get output similar to mine, you will need to [https://nodejs.org/en/download/](install Node.js and NPM) before you continue with this tutorial.
- Create a new React project by running:
npx create-react-app
For example:
npx create-react-app graphcms_blog_example
- After the installation process concludes, open up the project directory in your favorite code editor.
- We’ll need a couple of components for our blog. To start, we’ll need:
- A Home component in a views folder.
- A Navbar component in a components folder. Create these folders and their respective files.
- Next, let’s write dummy code for our Home component:
import React from 'react'; const HomeView = props => {
return(
<>
This is Home
);
};
- We’ll now import out Home component in the App.js folder:
import React from `react`;
import './App.css'; import HomeView from './views/Home'; function App() {
return (
);
}; export default App;
We can test our application now by opening our project directory in a terminal starting up a development server by running:
npm start
The development server may take a few moments to start up, but once it does, you’ll be able to view your application in a browser if you navigate to localhost:3000.
You should see This is Home displayed in the webpage.
Next, create the Navbar component. To make the development process easier, we’ll use Bootstrap, the world’s most popular web UI library, to quickly effect styles with minimal CSS.
Install Bootstrap in your project by running this in your terminal:
npm install --verbose bootstrap
After Bootstrap is installed, add the following line to the App.js file, at any point before the App function:
import 'bootstrap/dist/css/bootstrap.min.css';
You can have this directly below the line that says
import './App.css';
Back to the Navbar component. We want our navigation menu to be simple. There should be a logo (or brand name) to the far left, a group of links to the far right, and nothing in-between. Of course, if you can style your Navbar however you want. This is just the UI style we’ll be aiming for in this tutorial.
Since we’ll be adding a Dashboard view later on, which is where we’ll create and edit posts, we’d better set up routing for our app right away. This was, we’ll have working navigation in our web app.
We’ll use React-Router-DOM for our routing:
npm install --verbose react-router-dom
After the installation is finished, we’ll need to add routing capability to our project by editing App.js thus:
import React from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import HomeView from './views/Home';
function App() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={HomeView}
</switch>
</BrowserRouter>
);
};
export default App;
We can now continue with our Navbar component:
import React from 'react';
import { NavLink } from 'react-router-dom';
const Navbar = props => (
<div className="d-flex flex-row justify-content-between pt-4 pb-2 bg-secondary" id="navbar">
<div className="ml-4 brand-icon p-2 text-danger">
Martian Times
</div>
<div className="mr-4" style={{ width: "25%" }}>
<ul className="list-unstyled d-flex flex-row justify-content-between" style={{ width: "100%" }}>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/dashboard">Dashboard</NavLink>
</li>
<li>
<NavLink to="/about">About</NavLink>
</li>
</ul>
</div>
</div>
);
export default Navbar;
Import the Navbar component in the App.js component:
import React from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import Navbar from './components/Navbar';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import HomeView from './views/Home';
function App() {
return (
<BrowserRouter>
<Navbar />
<Switch>
<Route exact path="/" component={HomeView} />
</switch>
</BrowserRouter>
);
};
export default App;
Now, we need to connect our app with our GraphCMS backend. We’ll need a couple of libraries:
- graphql, to use GraphQL (which GraphCMS is built atop).
- graphql-tag, to be able to be able to create GraphQL queries for retrieving and mutating content.
- react-apollo, a popular React.js library for Apollo. Provides a couple useful components for accessing query results, etc.
- apollo-boost, will allow us to create a GraphQL client in our application. npm install –verbose graphql graphql-tag react-apollo apollo-boost
Now, we’ll need to create a GraphQL client that we can use to query posts from our GraphCMS endpoint. We can do this directly in the index.js file:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import dotenv from 'dotenv';
import { ApolloProvider } from 'react-apollo';
import ApolloClient from 'apollo-boost';
dotenv.config();
const apollo_client = new ApolloClient({
uri: process.env.REACT_APP_GCMS_URL
});
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={ apollo_client }>
<App />
</ApolloProvider>
</React.StrictMode>
);
serviceWorker.unregister();
Next, install dotenv…
npm install --verbose dotenv
Create a file in the project’s root directory named ‘.env’. Add the following to .env:
REACT_APP_GCMS_URL=YOUR_GRAPHCMS_ENDPOINT
You can find your GraphCMS endpoint by navigating to Settings > API Access in your GraphCMS dashboard.
Restart the development server by entering Control + C (Command + C on Macs) and:
npm start
Make sure the app still works without errors before you continue.
We now have a GraphQL client running and pointed to our GraphCMS endpoint.
Querying Content And Displaying Posts
Querying content is as easy as writing a schema for the properties (of each post) we want and asing the GraphCMS server to return those properties for each post. This feature of GraphQL server-client relationships makes it so powerful.
Create a new folder services in the project’s root directory. Then create a new file graphcms.js.
We’ll have all our content querying and mutating code in a class in this file. The idea is to be able to move all our code by copying a single class file. This way, we can reuse code across projects, edit/update code and manage our API-specific interactions within one file.
Create a class GraphCMSContent and add a fetchPosts method thus:
import gql from 'graphql-tag';
import { GraphQLClient } from 'graphql-request';
import dotenv from 'dotenv';
export default class GraphCMSContent {
constructor() {
dotenv.config();
this.Client = new GraphQLClient(
process.env.REACT_APP_GCMS_URL,
{
headers: {
authorization: `Bearer ${process.env.REACT_APP_GCMS_AUTH}`
}
}
);
};
fetchPosts(){
const QUERY = gql`
query {
posts {
id
title
body
author
createdAt
coverImage {
id
url
}
trending
}
}
`;
return QUERY;
};
};
All we are doing is:
- Creating a new GraphQL client just for this class. It’s supposed to be reusable, remember? The client is created whenever a new object is instantiated from this class, so we have the client-creation code in the class’ constructor method.
- Creating a fetchPosts method that we can call wherever we need to to fetch posts from GraphCMS.
- Specifying the properties of each post we want a fetchPosts function call to return.
We can now fetch posts into the Home component and display them.
Displaying Posts In The Home Component
We want our Home view to contain:
- A main area, where every post in the blog is displayed; and
- A sidebar, where trending posts in the blog are displayed.
We’ll create these areas:
import React from 'react';
const HomeView = props => {
return (
<>
<div className="container row mx-auto p-2">
<div className="col-8">
MAIN AREA
</div>
<div className="col-3 offset-2">
SIDEBAR
</div>
</div>
</>
);
};
export default HomeView;
Next, we want to display a list of the first six posts in the blog. We’ll create a PostPreview component that we can reuse. In the root directory, create a new folder components and create a new file postPreview.js:
import React from 'react';
import DummyJPEG from '../assets/dummy.jpeg';
const PostPreview = ({post, noImage}) => {
console.log(noImage)
return (
<div className="post-preview">
{
(() => {
if (noImage !== true) {
return (
<div className="cover-image-wrapper">
<img className="cover-image" src={ post.coverImage !== null ? post.coverImage.url : DummyJPEG } />
</div>
)
} else {
return null;
}
})()
}
<span className="read-more-btn">Read</span>
<div>
<h5 className="title">{ post.title }</h5>
<div className="text-center">
<span className="tag">Author</span> <span className="author">{ Array.isArray(post.author) ? post.author.join(", ") : post.author }</span>
</div>
</div>
<div className="body">
{ post.body }
</div>
</div>
);
};
export default PostPreview;
In the Home component:
import React from 'react';
import { Query } from 'react-apollo';
import PostsPreview from '../components/postPreview';
import GraphCMSContent from '../services/graphcms';
const Home = props => {
const [posts, setPosts] = React.useState([]);
const Client = new GraphCMSContent();
const LoadingPostsJsx = () => (
<div>
Loading...
</div>
);
const ErrorLoadingPostsJsx = () => (
<div className="mx-auto alert-danger">
Error!
</div>
);
return (
<>
<div className="container p-2 mx-auto row">
<div className="col-8">
<h3>Recent Articles</h3>
<div className="border p-3">
<Query query={Client.fetchPosts()}>
{
({loading, error, data}) => {
if (loading) return LoadingPostsJsx();
if (error) {
console.log(error);
return ErrorLoadingPostsJsx();
}
const POSTS = data.posts;
setPosts(POSTS);
return POSTS.slice(0,6).map(post => (
<PostsPreview post={post} />
));
}
}
</Query>
</div>
</div>
<div className="col-4 border bg-secondary p-2 sidebar">
TRENDING POSTS HERE...
</div>
</div>
</>
);
};
export default Home;
What We Are Doing:
- We import the Query component from react-apollo which will enable us to access query results.
- Next, we create a posts state with the useState hook (all about hooks here https://reactjs.org/docs/hooks-intro.html).
- We create a new GraphCMSContent object (Client).
- We define functions for returning specific JSX while posts data is being retrieved or if an error occurs in the API transaction.
- Finally, we query the GraphCMS endpoint and display the retrieved posts.
Check that this works fine in your browser. If the error component is displayed, check:
1. Your network connection.
2. Your **.env** file, that your variable names are correct and their values match data from your dashboard.
In order to have the same UI styling as I do, you’ll need to paste the following in your App.css stylesheet file:
#navbar {
border-bottom: 1px solid #444;
background-color: pink;
}
#navbar .brand-icon {
background-color: #999;
border-radius: 6px;
color: white;
}
.post-preview {
height: 100%;
max-height: 650px;
overflow: hidden;
margin-bottom: 2.5em;
border: 1px solid #efefef;
border-radius: 4px;
padding: 10px;
background-color: lightgray;
position: relative;
}
.post-preview > .cover-image-wrapper {
object-fit: contain;
height: 290px;
text-align: center;
}
.post-preview > .cover-image-wrapper > img {
width: 100%;
height: 100%;
}
.post-preview .title {
text-align: center;
font-weight: 600;
color: black;
font-size: 160%;
font-family: 'Times New Roman', Times, serif;
margin: 20px auto 10px;
}
.post-preview .author {
color: grey;
background-color: #efefef;
border-radius: 14px;
padding: 6px;
font-size: 80%;
}
.post-preview .tag {
font-size: 90%;
font-weight: 600;
color: black;
}
.post-preview .body {
margin-top: 1em;
color: #666;
}
.post-preview .read-more-btn {
position: absolute;
top: 10px;
right: 10px;
background-color: lightpink;
color: #444;
padding: 1px 9px;
border-radius: 10px;
font-size: 80%;
cursor: pointer;
}
.sidebar {
max-height: 160vh;
overflow-y: scroll;
}
Displaying Trending Posts
Displaying trending posts is a simple matter of filtering available posts based on whether the trending property (we defined this in our GraphCMS dashboard Content and the fetchPosts schema, remember?) is set to true or not. You can create a couple more posts in your GraphCMS dashboard and set their trending properties to true so that you’ll be able to test this.
To really streamline the process, and for the sake of code reusability, we’ll need a component specifically for displaying trending posts.
In the components folder, create a new file Trending.js:
Add the following code to Trending.js:
import React from 'react';
import PostPreview from './postPreview';
const TrendingPosts = ({posts}) => {
return (
<div>
<h5 className="text-white text-center">Trending Posts</h5>
{
posts.filter(post => post.trending == true).map(post => {
<div style={{ maxHeight: "210px", overflow: "hidden", marginBottom: "1em" }}>
<PostPreview post={post} noImage />
</div>
})
}
</div>
);
};
export default TrendingPosts;
What We Are Doing:
- We import the PostPreview component.
- We filter the posts (which we receive from the parent component — the HomeView component, in this case — via the posts prop.
- Using the map() function, we invoke the PostPreview component for each post in the filtered posts array. We pass the noImage prop because we do not want images to be displayed in the trending-posts sidebar.
- Finally, we return these back to the parent component.
Replace the sidebar in the HomeView component with the following:
<div className="col-4 border bg-secondary p-2 sidebar">
<TrendingPosts posts={posts} />
</div>
Our client app now works perfectly:
Developing The Dashboard View For Creating And Editing Posts
We need a way to create and edit content without needing to use the GraphCMS dashboard directly. There are a several reasons why this is important:
- We are in an organization and need to have different access levels for different roles.
- We need to provide abstraction for a client so that she won’t run the risk of breaking something by handling the GraphCMS dashboard directly.
- We need to integrate custom/external APIs or other web services in our client-server pipeline.
- We need to make content management more accessible. For example, by developing a mobile app.
GraphCMS allows us to get all these done via the GraphCMS Mutation API.
We’ll start by creating the Dasboard view.
Creating The Dashboard View
First, create a new file dashboard.js within the views folder:
Scaffold a functional component in the Dashboard.js file:
import React from 'react';
const DashboardView = props => {
return (
<>
This is the dashboard
</>
);
};
export default DashboardView;
Add a route to the dashboard in App.js:
import React from 'react';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import Navbar from './components/Navbar';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import HomeView from './views/Home';
import Dashboard from './views/Dashboard';
function App() {
return (
<BrowserRouter>
<Navbar />
<Switch>
<Route exact path="/" component={HomeView} />
<Route path="/dashboard" component={Dashboard} />
</switch>
</BrowserRouter>
);
};
export default App;
Test the route by clicking Dashboard in the navigation menu.
The Dashboard view will have two main sections:
- A main area with a form for creating new posts.
- A sidebar where the titles of every published post are displayed. Clicking a title will call up a modal for editing/updating the post.
We’ll start with creating new posts.
Creating New Posts
Add a createPost method to the GraphCMSContent class:
async createPost({title, body, author}) {
const QUERY = gql`
mutation {
createPost(data: {
title: "${title}",
author: "${author}",
body: "${body}"
}) {
id
title
body
author
}
}
`;
try {
const data = await this.Client.request(QUERY);
return data;
} catch(error) {
console.log("Error at createPost:>>", error);
return false;
}
}
This is an asynchronous function that expects a title, a body and an author for the new post.
GraphCMS mutations are effected by creating a GraphQL query (using the qql tag and specifying “mutation” as the type of operation to be performed — in contrast to ”query” in our fetchPosts query). Then we specify the properties of the new item to be created by using the create__ function that GraphCMS exposes.
The create__ function is a special function that GraphCMS automatically creates for us whenever we create a new model. It’s name is always created by appending the name of the model to create. In our case, we called our model Post, hence the createPost function.
It is important to understand that if we try to use a “create__” function that does not exist, our queries will fail.
For example, if a model is called “Quiz”, the create_ function must be called “createQuiz”. “createQuizz”, “createQuizzes” and “createNewQuiz” will not be recognized.
We are interested only in the post id, title, body and author. Of course, you can add a cover image (property: coverImage) but I do not want to complicate things by handling file uploads.
const QUERY = gql`
mutation {
createPost(data: {
title: "${title}",
author: "${author}",
body: "${body}"
}) {
id
title
body
author
}
}
`;
The GraphQLClient class (imported from **graphql-request) exposes a *request* method that we can use to make requests. This request process is an asynchronous operation, so we wrap it within a try…catch block and “catch” any errors that might occur. This can also be done using the promise syntax.
try {
const data = await this.Client.request(QUERY);
return data;
} catch(error) {
console.log("Error at createPost:>>", error);
return false;
}
Or:
return this.Client.request(QUERY)
.then(data => data)
.catch(error => {
console.log("Error at createPost:>>", error);
return false;
});
Update Dashboard.js with the following:
import React from 'react';
import { Query } from 'react-apollo';
import GraphCMSContent from '../services/graphcms';
const Dashboard = props => {
const [posts, setPosts] = React.useState([]);
const [newPost, setNewPost] = React.useState({});
const Client = new GraphCMSContent();
const LoadingPostsJsx = () => (
<div>
Loading...
</div>
);
const ErrorLoadingPostsJsx = () => (
<div className="mx-auto alert-danger">
Error!
</div>
);
const createPost = async () => {
const res = await Client.createPost(newPost);
console.log(res);
if (res !== false) alert("New Post Created Successfully!");
else
alert("An error occurred!");
};
return (
<div id="dashboard_view">
<div className="container p-2 mx-auto row">
<div className="col-8">
<form onSubmit={ e => { e.preventDefault(); createPost(); } } className="form col-12">
<div>
<label className="small col-12">Title
<input onChange={ e => setNewPost({
...newPost,
title: e.currentTarget.value
}) } className="form-control" required />
</label>
</div>
<div>
<label className="small col-12">Author
<input onChange={ e => setNewPost({
...newPost,
author: e.currentTarget.value
}) } className="form-control" required />
</label>
</div>
<div>
<label className="small col-12">Content<br />
<textarea onChange={ e => setNewPost({
...newPost,
body: e.currentTarget.value
}) } className="form-control" cols="90" style={{ height: '160px' }}
required ></textarea>
</label>
</div>
<div className="text-center">
<button type="submit" className="btn btn-success">Create Post</button>
</div>
</form>
</div>
<div className="col-3 offset-1">
<h4 className="text-center text-danger">
All Posts: {posts.length}
</h4>
POSTS WILL GO HERE
</div>
</div>
</div>
);
};
export default Dashboard;
The Dashboard view should now have a nice-looking form for adding new posts. Enter dummy content and click Create Post. If all goes well, a new post should be created. New posts won’t automatically show up in the app because they are still drafts, but you should be able to see them in the Content tab of your GraphCMS dashboard.
Click that pencil and click Publish to publish your post.
Reload the Home view to see your newly published post.
It is possible to publish posts without going to the GraphCMS dashboard directly, but we won’t be doing this in this tutorial. You can read the GraphCMS documentation to learn how.
Updating Previously Published Posts
Updating posts is as simple as replacing a post’s content with a new one and mutating the GraphCMS store with the change. The post becomes a draft again and can be re-published.
First, create an updatePost method in graphcms.js.
async updatePost(post_id, { title, author, body }) {
const QUERY = gql`
mutation {
updatePost(
where: { id: "${post_id}" }
data: { title: "${title}", author: "${author}", body: "${body}" }
) {
id
title
body
author
}
}
`;
try {
const data = await this.Client.request(QUERY);
return data;
} catch(error) {
console.log("Error at updatePost:>>", error);
return false;
}
};
Like the create__ function, the update__ function is automatically created by GraphCMS whenever a model is created.
The update__ method requires two arguments: a where clause that tells which post to update (referenced by an id), and a data object that holds the data to replace the old content with:
const QUERY = gql`
mutation {
updatePost(
where: { id: "${post_id}" }
data: { title: "${title}", author: "${author}", body: "${body}" }
) {
id
title
body
author
}
}
`;
Thereafter, updating a post is as simple as making a request using the request method of the GraphQLClient class.
try {
const data = await this.Client.request(QUERY);
return data;
} catch(error) {
console.log("Error at updatePost:>>", error);
return false;
}
To get data with which to update a post, we’ll need a form.
First, we’ll display all posts in the sidebar. Clicking a post will call up a modal containing the form. React-Bootstrap provides a nice easy-to-use modal that we can leverage.
Install React-Bootstrap:
npm install --verbose react-bootstrap
Replace the Dashboard.js component with:
import React from 'react';
import { Query } from 'react-apollo';
import GraphCMSContent from '../services/graphcms';
import Modal from 'react-bootstrap/Modal';
const Dashboard = props => {
const [posts, setPosts] = React.useState([]);
const [newPost, setNewPost] = React.useState({});
const Client = new GraphCMSContent();
const [showUpdateModal, setShowUpdateModal] = React.useState(false);
const [postToUpdate, setPostToUpdate] = React.useState('');
const [replacementPost, setReplacementPost] = React.useState({
title: '', author: '', body: ''
});
const LoadingPostsJsx = () => (
<div>
Loading...
</div>
);
const ErrorLoadingPostsJsx = () => (
<div className="mx-auto alert-danger">
Error!
</div>
);
const createPost = async () => {
const res = await Client.createPost(newPost);
console.log(res);
if (res !== false) alert("New Post Created Successfully!");
else
alert("An error occurred!");
};
const activateUpdateModal = post_id => {
setPostToUpdate(post_id);
setShowUpdateModal(true);
};
const handleUpdate = async () => {
const res = await Client.updatePost(postToUpdate.id, replacementPost);
if (res !== false) alert(`Successfully updated post with ID: ${postToUpdate.id}`);
else
alert("An error occurred while attempting to update a post");
};
return (
<div id="dashboard_view">
<div className="container p-2 mx-auto row">
<div className="col-8">
<form onSubmit={ e => { e.preventDefault(); createPost(); } } className="form col-12">
<div>
<label className="small col-12">Title
<input onChange={ e => setNewPost({
...newPost,
title: e.currentTarget.value
}) } className="form-control" required />
</label>
</div>
<div>
<label className="small col-12">Author
<input onChange={ e => setNewPost({
...newPost,
author: e.currentTarget.value
}) } className="form-control" required />
</label>
</div>
<div>
<label className="small col-12">Content<br />
<textarea onChange={ e => setNewPost({
...newPost,
body: e.currentTarget.value
}) } className="form-control" cols="90" style={{ height: '160px' }}
required ></textarea>
</label>
</div>
<div className="text-center">
<button type="submit" className="btn btn-success">Create Post</button>
</div>
</form>
</div>
<Modal show={ showUpdateModal } onHide={ e => setShowUpdateModal(false) }>
<Modal.Header closeButton>
Update Post
</Modal.Header>
<Modal.Body>
<form onSubmit={ e => { e.preventDefault(); handleUpdate(); } }>
<label>
Title:
<input onChange={ e => setReplacementPost({ ...replacementPost, title: e.currentTarget.value }) } className="form-control" />
</label>
<label>
Author:
<input onChange={ e => setReplacementPost({ ...replacementPost, author: e.currentTarget.value }) } className="form-control" />
</label>
<label>
Body:
<textarea onChange={ e => setReplacementPost({ ...replacementPost, body: e.currentTarget.value }) } className="form-control"></textarea>
</label>
<div className="text-center mt-2">
<button className="btn btn-danger">Update</button>
</div>
</form>
</Modal.Body>
</Modal>
<div className="col-3 offset-1">
<h4 className="text-center text-danger">
All Posts: {posts.length}
</h4>
<Query query={Client.fetchPosts()}>
{
({loading, error, data}) => {
if (loading) return LoadingPostsJsx();
if (error) {
console.log(error);
return ErrorLoadingPostsJsx();
}
const POSTS = data.posts;
setPosts(POSTS);
return posts.map(post => (
<div onClick={ e => activateUpdateModal(post.id) } className="pl-2 text-muted border mb-3" style={{ cursor: 'pointer' }}>
{ post.title }
</div>
))
}
}
</Query>
</div>
</div>
</div>
);
};
export default Dashboard;
Test the app in the browser. Titles of each published post should now be displayed in the sidebar, and clicking a post should reveal a modal containing a form that can be used to edit that post.
After editing a post, you will need to re-publish it from within the GraphCMS dashboard.
What We Are Doing:
- First, we import the necessary components:
import React from 'react';
import { Query } from 'react-apollo';
import GraphCMSContent from '../services/graphcms';
import Modal from 'react-bootstrap/Modal';
Remember, the Query component allows us to access the results of a query: it allows us to manipulate query results as JSX.GraphCMSContent is the special class we defined in the services folder, andModal is simply the modal component that React-Bootstrap provides. Since this is a prototype, we can justify installing a library just for one small function. In production systems, we might need to build a modal component from scratch in order to reduce deadweight, if this is the only reason we need React Bootstrap.
- Next, we define several bits of state that will help us manage the content creation/update process:
const [posts, setPosts] = React.useState([]);
// an array of posts: to be displayed in the sidebar
const [newPost, setNewPost] = React.useState({});
// holds the content of the Create Post form: each time an input field's value changes, this state is updated with the new value
const Client = new GraphCMSContent();
// our GraphCMSContent client
const [showUpdateModal, setShowUpdateModal] = React.useState(false);
// a boolean: whether to show the Update Post modal or not
const [postToUpdate, setPostToUpdate] = React.useState('');
// holds the ID of the post that is being updated
const [replacementPost, setReplacementPost] = React.useState(
{
title: '', author: '', body: ''
}
);
// holds the content of the Update Post form: will be used to replace the post whose ID is saved to postToUpdate
- The next thing we do is define a couple of important functions. These functions are responsible for handling form input and calling the API services in our GraphCMSContent class.
- There are:
- createPost, an asynchronous function: calls the createPost service. I have logged the response from Client.createPost so that you can see what the response object looks like. Basicaly, if Client.createPost returns false, an error occurred and an alert is shown. Any other response value is a success.
const createPost = async () => {
const res = await Client.createPost(newPost);
console.log(res);
if (res !== false) alert("New Post Created Successfully!");
else
alert("An error occurred!");
};
- ii. activateUpdateModal: is called when a post in the sidebar is clicked. It updates the postToUpdate state with the relevant post ID, which it receives from the onclick event prop of the clicked post, and sets showUpdateModal to true. When this happens, the modal is automatically displayed.
- The post ID in postToUpdate will be used by the updatePost service (in the where parameter of the query) to update the selected post.
const activateUpdateModal = post_id => {
setPostToUpdate(post_id);
setShowUpdateModal(true);
};
- iii. handleUpdate, an asynchronous function. handleUpdate interfaces with the updatePost service. It invokes the service with the ID of the post to be updated, as well as the content of the new post that will replace the old one. If updatePost returns false, handleUpdate shows an alert with an error message, otherwise it shows a success alert.
const handleUpdate = async () => {
const res = await Client.updatePost(postToUpdate, replacementPost);
if (res !== false) alert(`Successfully updated post with ID: ${postToUpdate}`);
else
alert("An error occurred while attempting to update a post");
};
Conclusion And Final Words
Knowing the right tools for a job is a skill that software developers are often called upon to use, and GraphQL + GraphCMS is one of the best combinations right now for solving content management problems. These together can be used to create powerful backends that are fast, light, and component-driven. Throwing in a powerful UI library like React can give a project development team so much freedom: freedom to create. GraphCMS, like React and GraphQL, makes no assumptions about the kind of project or its specifications, it enforces very little on the developer. This makes it a very flexible option for building content management backends that can scale. You can access the source code on Github.