This article originally appeared on the Cosmic JS Blog.
In this tutorial I’m going to show you how to create a simple “ToDo” app using React, Redux, a little bit of Node, and Cosmic JS. For the sake of understanding how to consume Restful API’s, this tutorial will show how to make simple AJAX requests to the Cosmic JS API in order to retrieve, update, and delete data in our Cosmic JS buckets. Let’s get started.
Download the GitHub repo.Check out the demo.
First, let’s make a new directory to build our project in and lets also make a package.json file.
mkdir cool-cosmic-todo-app
cool-cosmic-todo-app$ touch package.json
Now, in your package.json, copy and paste the code below and then run npm install or yarn (if you’re a super cool yarn user):
//cool-cosmic-todo-app/package.json{ "name": "cosmicToDo", "version": "1.0.0", "description": "A simple todo app thet uses the Cosmic JS API",
Now we’re going to build out our file structure a bit more so that we can organize our react components and redux files. This is what our cool-cosmic-todo-app directory should look like:
cool-cosmic-todo-app|----client| |----components| |----Home.js| |----Section.js| |----Task.js| |----index.html| |----dist|----redux| |----store.js| |----reducer.js|----server| |----index.js|----webpack.config.js|----package.json
Now we we will set up our index.html in our client directory. Copy and paste the following code into your index.html file:
<!DOCTYPE html><html><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Cosmic Todo App!</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <link rel="stylesheet" href="./styles/mainSheet/main.css"> <link rel="stylesheet" href="./styles/mainSheet/font-awesome.min.css"> <script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script> <script src="/dist/bundle.js" defer></script></head><body><div id="root"></div></body></html>
Here, we are going to target our “root” div to place our react components in later. The bundle.js file located in our dist directory is what our webpack.config file will spit out after bundling all of our react components Now, set up our webpack.config file to bundle all of our react files and export that bundle file to our dist directory. Copy the following code into your webpack.config.js file:
let path = require('path');let LiveReloadPlugin = require('webpack-livereload-plugin');
module.exports = { entry: './client/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'client/dist') }, context: __dirname, resolve: { extensions: ['.js', '.jsx', '.json', '*'] }, module: { rules: [{ test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, loader: 'babel-loader', options: { presets: ['react', 'es2015'] } }, { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] }] }, plugins: [ new webpack.DefinePlugin({ 'process.env.COSMIC_BUCKET': JSON.stringify(process.env.COSMIC_BUCKET), 'process.env.COSMIC_READ_KEY': JSON.stringify(process.env.COSMIC_READ_KEY), 'process.env.COSMIC_WRITE_KEY': JSON.stringify(process.env.COSMIC_WRITE_KEY) }), new LiveReloadPlugin({appendScriptTag: true}) ]};
Here, we are simply telling webpack to output a bundle.js file into our dist folder with all of our react components bundled in there as well. The babel-loader is simply transpiling JavaScript files using babel and webpack. The live reload plugin allows us to make changes to our files and automatically have webpack watch and “re-bundle” with our updated changes. Next, we’re going to us a little bit of node with express to serve up our index.html file as well as our bundle.js file. Realistically, we aren’t really making requests to our own node backend, but it’s good to use node as a method to serve us static files. Copy and paste the code below into our index.jsfile located in our server directory.
const express = require('express');const app = express();const path = require('path');const volleyball = require('volleyball');
app.use(volleyball);
//serve up static filesapp.use(express.static(path.resolve(__dirname, '..', 'client')));app.use(express.static(path.resolve(__dirname, '..', 'node_modules')));
app.use(function (err, req, res, next) { console.error(err); console.error(err.stack); res.status(err.status || 500).send(err.message || 'Internal server error.');});
// handle every other route with index.html, which will contain// a script tag to our application's JavaScript file(s).app.get('*', function (request, response) { response.sendFile(path.resolve(__dirname, '..', 'client', 'index.html'))});
//listen on port 3000app.listen(process.env.PORT || 3000, function () { console.log("Rockin' out on port 3000 homie");});
Here, we are simply telling our server to serve up static files out of our node_modules and client directories. The ‘*’ route is basically telling express to serve up our index.html on any route requested within our app. Finally, we are telling express to listen on port 3000. Feel free to try it out! Run npm start or yarn start and see our “Rockin’ out on port 3000 homie” message log put to the console! Next, let’s set up our redux store.js file, located in our client/redux folder. Copy and paste the following code into the store.js file located in our client/redux folder:
import { createStore, applyMiddleware } from 'redux';import reducer from './reducer';import thunkMiddleware from 'redux-thunk';import {createLogger} from 'redux-logger';
const store = createStore( reducer, applyMiddleware( createLogger(), thunkMiddleware ));
export default store;
We are setting up our redux “store” so that we can pass data down through “props” into whatever components we want (I am going to assume you have some working knowledge of how the flux architecture functions, if you do not understand this it is OK, just check the flux architecture link above).
We are using the redux-thunk middleware to handle all of our asynchronous calls to our Cosmic JS API. This middleware is used whenever we dispatch a function instead of an action.
We are also using “createlogger” middleware to visually see every logged action that we dispatch. Next, we are going to set up the index.js file, located in our client directory. Copy and paste the following code:
import React from 'react';import ReactDOM from 'react-dom';import Home from './components/Home';import {Provider} from 'react-redux';import store from './redux/store';
ReactDOM.render( <Provider store={store}> <Home /> </Provider>, document.getElementById('root'));
We are telling ReactDOM to target our “root” div from our index.html file and pop in our <Home/> react component (which we will build out in our next step).
Our react component is being served up data from our store through that tag. It is “providing” its children with data (hence, why it’s called “Provider”). Next, we will build out our Homecomponent. In our Home.jsfile located in out client/components directory, copy and paste the following code:
import React, {Component} from 'react';import { connect } from 'react-redux';import { getAllTasks, postNewTask } from '../redux/reducer';import Task from './Task';
class Home extends Component { constructor(props){ super(props); } componentDidMount(){ getAllTasks(); } render() { return ( <div> <div className="container"> <div className="row"> <div className="col-xs-12"> <h1>Cosmic To-Do App!!</h1> <form onSubmit={evt => { evt.preventDefault(); this.props.postNewTask(evt.target.taskName.value); evt.target.taskName.value = ""; } }> <div className="form-group"> <label for="exampleInputEmail1">Add New To-Do</label> <input name="taskName" placeholder="Enter new task" /> </div> <button type="submit">Add</button> </form> </div> </div> </div> <div className="container"> <div className="row"> <div className="col-xs-12"> <h3>Let's get some work done!</h3> </div> </div> </div> <div className="container"> { this.props.tasks && this.props.tasks.map((task) => { return ( <Task key={task._id} Obj={task} isComplete={task.metafields[0].value} Name={task.title}/> ) }) } </div> </div> ) }}
const mapState = ({tasks}) => ({tasks});const mapDispatch = {getAllTasks, postNewTask};export default connect(mapState, mapDispatch)(Home);
We are creating a react component and fetching all of our data when our Home component mounts (which will be tasks form our Cosmic JS bucket).
We are rendering “HTML” to our virtual DOM.
We are creating a form that will dispatch a function (that will go through our redux-thunk middleware) that adds tasks to our list of tasks to do whenever our users click the submit <button>.
We are doing a hacky check so that our app does not break by doing what is called a “short-circuit”. We are first checking if we have gotten back “tasks” from our asynchronous call to our Cosmic JS bucket and if we do then we are going to map over each of them and render a <Task /> component (which we will build in the nex step).
Finally, we are wrapping our Home component inside of the connect function that the react-redux library gives us. We are getting the {tasks} from our store as well as our {getAllTasks, postNewTask} functions from our store. Next, we are going to build out our component to render every task that we are getting from our store. In our Task.js file, located in our client/components directory, copy and paste the following code:
import React from 'react';import { connect } from 'react-redux';import { putChangeStatus, deleteTask } from '../redux/reducers/reducer';
const Task = (props) => { return ( <div className="row"> <div className="btn-group" role="group" aria-label="Basic example"> <button type="button" onClick={() => { props.putChangeStatus(props.Obj, props.isComplete)}} className="btn">{props.isComplete ? "Undo" : "Complete" }</button> <button type="button" onClick={() => props.deleteTask(props.Obj.slug)} className="btn">Delete</button> </div> <h3 style={{textDecoration: props.isComplete ? "line-through" : "none"}}>{props.Name}</h3> </div> );};
const mapDispatch = {putChangeStatus, deleteTask};export default connect(null, mapDispatch)(Task);
We are creating what is called a “dumb” react component and, again, wrapping it in our connect function.
We are pulling in putChangeStatus and deleteTask from our reducer (which we will finally make in te next step) and hookingit up to our store.
We are hooking up our “Complete” and “Delete” buttons to dispatch asynchronous functions onClick so that we actually update our Cosmic JS bucket as well. Next, we will FINALLY make our reducer and set up all of our asynchronous “redux-thunk” functions. In our reducer.js file, located in our client/redux directory, copy and paste the following code:
import axios from "axios";/////////////////CONSTANTS/////////////////////const GET_ALL_TASKS = "GET_ALL_TASKS";const POST_TASK = "POST_TASK";const CHANGE_STATUS = "CHANGE_STATUS";const DELETE_TASK = "DELETE_TASK";/////////////////ACTIONS//////////////const getTasks = (tasks) => ({type: GET_ALL_TASKS, tasks});const addTask = (task) => ({type: POST_TASK, task});const changeStatus = (task) => ({type: CHANGE_STATUS, task});const taskDelete = (slug) => ({type: DELETE_TASK, slug});/////////////////REDUCER///////////////////////initiate your starting statelet initial = { tasks: []};const reducer = (state = initial, action) => { switch (action.type) { case GET_ALL_TASKS: return Object.assign({}, state, {tasks: action.tasks.objects}); case POST_TASK: let updatedTasks = [action.task].concat(state.tasks); return Object.assign({}, state, {tasks: updatedTasks}); case CHANGE_STATUS: let newArr = state.tasks.map((task) => { if(task.slug === action.task.slug) task.metafields[0].value = !task.metafields[0].value; return task; }); return Object.assign({}, state, {tasks: newArr}); case DELETE_TASK: let arr = state.tasks.filter((task) => { return !(task.slug === action.slug); }); return Object.assign({}, state, {tasks: arr}); default: return state; }
};
export default reducer;
/////////////// ACTION DISPATCHER FUNCTIONS///////////////////
export const getAllTasks = () => dispatch => { axios.get(https://api.cosmicjs.com/v1/your-bucket-slug-name/object-type/tasks
) .then((response) => { return response.data; }) .then((tasks) => { dispatch(getTasks(tasks)) }) .catch((err) => { console.error.bind(err); })};
export const postNewTask = (task) => dispatch => { dispatch(addTask({title: task, metafields: [{value: false}], slug: formatSlug(task)})); axios.post(https://api.cosmicjs.com/v1/your-bucket-slug-name/add-object
, {type_slug: "tasks", title: task, content: "New Task", metafields: [ { title: "Is Complete", key: "is_complete", value: false, type: "text" } ]}) .then((response) => { console.log(response.data); }) .catch((err) => { console.error.bind(err); })};
export const putChangeStatus = (task, bool) => (dispatch) => { dispatch(changeStatus(task)); axios.put(https://api.cosmicjs.com/v1/your-bucket-slug-name/edit-object
, {slug: task.slug, metafields: [ { title: "Is Complete", key: "is_complete", value: !bool, type: "text" } ]}) .then((response) => { console.log(response.data); }) .catch((err) => { console.error.bind(err); })};
export const deleteTask = (slug) => (dispatch) => { dispatch(taskDelete(slug)); axios.delete(https://api.cosmicjs.com/v1/your-bucket-slug-name/${slug}
) .then((response) => { console.log(response.data) }) .catch((err) => { console.error.bind(err); })};
const formatSlug = (title) => { let lower = title.toLowerCase(); return lower.split(" ").join("-");};
Side note -> I personally like to store all of my constants, actions, and action dispatcher functions inside of one file for every reducer that I have just so I’m not contantly jumping from file to file. It’s totally common and cool to break these up into seperate files.
We were able to consume the Cosmic JS API with our actions and dispatcher functions. If you’re a little iffy on how how all the files work in conjunction with our store check out this gif to get a better picture of how redux is passing data around the app. I hope you enjoyed this tutorial as much as I did, if you have any questions reach out to us on Twitter and join our community on Slack.