Part 3: Stop writing mapStateToProps, start using declarative models

Kyle Ramirez
14 min readNov 27, 2017

Please also read Part 1 and Part 2 of this story, where I talk about the background of Reactive Record, and what it actually is.

This is a multi-part series which demonstrates introductory concepts on the uses of Reactive Record, an immensely useful utility for communicating with APIs in your single-page applications, using React and Redux. We’re actively working on the documentation, but you’re welcome to take a look on our Github.

If you’ve read Part 1 & 2, I hope you’re excited to learn not only what Reactive Record can do, but how you can quickly begin using it in your own application or sample application, to create, read, update and delete (CRUD) API resources in a declarative way. The CRUD model is imperative to any application. That’s all we’re doing all over the web, really. It can be applied to simple examples, e.g. ToDo’s, or with user logins (create session, destroy session, create password reset, destroy account). You’re going to have a lot of fun translating your app into its most RESTful representation.

Quick Vocab
API Resource — a thing, a book, a user, any object that can be sent as JSON.
API Endpoint — a predefined URL where an API resource can be located (/api/v2/users).
REST — (REpresentational State Transfer) The guiding principles on which lots of APIs are written, be it JSON, XML, Node … Erlang. It’s important here because it’s how I assume your application works … How it should work, if it doesn’t already. Research this if you’ve not heard of it.

I’m going to show you the bare bones markup required to get going, using an overarching example:

~ Maintaining a list of blog posts ~

In this part of the series, I’ll demonstrate:

  • Boilerplate: The bare minimum code needed to get ReactiveRecord working in a React/Redux build.
  • Models: How and where to define a model based on our overarching example (Posts).

I know it doesn’t seem like a lot for one article, but I want you to understand the core concepts, which I’ll bring full circle later when we’re writing declarative magic in React.

Prerequisites
This is not a tutorial on how to get Webpack setup, how to write an API, how to use React or Redux. It assumes a basic understanding of how these things work. When it comes to your API, it should respond to GET / POST / PUT / DELETE requests for predefined resources. If your website does not work this way, it’s time to spread your full-stack wings and implement a RESTful API. I mean, c’mon, how will you release your first Alexa skill, “Alexa, pay my rent!” without a RESTful API that hooks into AWS Lambda? This tutorial doesn’t require Webpack, but ReactiveRecord is written in ES6, and makes heavy use of the ES7 bind operator. There is an ES5 export, but I would highly recommend using Webpack with Babel for your build.

Luckily, we don’t need to build out an API for this example. We’re going to use a placeholder RESTful API backend provided by our goons at JSONPlaceholder. I built this example without ever taking a look at the JSONPlaceholder documentation, and it just worked. That’s REST for you. You can assume things work a certain way, and as long as everyone sips the same Kool-Aid, we can make incredibly useful tools for each other.

Boilerplate
To begin using ReactiveRecord, we need to install it in our codebase, install a single Redux reducer, and a single Redux middleware.

Installation

yarn install reactiverecord

In case you missed it, you’ll need react react-redux redux if you don’t have it already. If you use the babel Webpack loader with the exclude option to transform code only in your codebase, you’ll need to amend it like so:

/* from */ exclude: /node_modules/,
/* to */ exclude: /node_modules(?!\/reactiverecord).*$/,

Redux store setup
I want to show how simple it is to add ReactiveRecord to your build. It really doesn’t get more complicated than this:

👇🏾👇🏾👇🏾 Example Store (store.js)

import { createStore, applyMiddleware } from "redux"
import ReactiveRecord, { reducer, middleware } from "reactiverecord"
import Post from "./Post" /* import all your models */
const store = createStore(
ReactiveRecord::reducer(),
applyMiddleware(
ReactiveRecord::middleware()
)
)
ReactiveRecord.dispatch = store.dispatch /* Very important */

Reactive Record’s default export is already an instance of ReactiveRecord, so no need to call new for our purposes. Notice the use of the bind operator (::), which calls the reducer and middleware functions with ReactiveRecord as the context. This is the same as calling reducer.call(ReactiveRecord), which returns a reducer. Notice I’m not writing any extra reducers or remote AJAX boilerplate.

Because we’re going to be using the fake placeholder JSON api, we’ll need to prefix all API requests with the correct domain. This is also useful if all your API endpoints start with a prefix such as /api/v1. Add the following to your store.js file:

ReactiveRecord.setAPI({
prefix: "http://jsonplaceholder.typicode.com"
})

That’s really it! We can begin defining models and make declarative API requests!

Models
Models are the core of your application, and the core of ReactiveRecord. You need to define them and describe them before any rendering occurs. They can’t be generated dynamically, and as models contain business logic / describe schema, they typically aren’t generated dynamically. If you have never heard of a model or schema, you should read up on the MVC pattern before going any further. In a framework like Ruby-on-Rails, you define models in the folder /app/models. You can define Models anywhere you like on the front-end using ReactiveRecord, as long as they are imported before you create your Redux store. ReactiveRecord needs to know about the models in order to dynamically generate routes and reducers. You use models to define your resource specification:

  • Routes
  • Store configuration (singleton usage / vs collection)
  • Schema
  • Validations
  • custom setters / getters
  • custom instance / singleton methods

Once your models are defined, ReactiveRecord will begin treating them as first-class citizens across your application. Here is a full Post model per our overarching example:

👇🏾👇🏾👇🏾 Example Model (Post.js)

import ReactiveRecord, { Model } from "reactiverecord"class Post extends Model {
static schema = {
title: String,
body: String,
_timestamps: true
}
}
export default ReactiveRecord.model("Post", Post)

ReactiveRecord will now assume a few things based on what is described above:

1. Route interpolation
INDEX: The index API endpoint is GET /posts
CREATE: The create API endpoint is POST / posts
SHOW: The show API endpoint is GET /posts/:id
UPDATE: The update API endpoint is PUT /posts/:id
DESTROY: The destroy API endpoint is DELETE /posts/:id

2. Default primary key
Since we haven’t said otherwise, ReactiveRecord assumes this model is identified by an id field, which could be a Number or a String.

3. Automatic timestamps
Whether your backend calls it created_at or createdAt, ReactiveRecord will use createdAt, and accept both, without us having to manually define them as a Date data type.

It will automatically pluralize the model name to generate routes for API requests. So, if your model is called User, you can expect ReactiveRecord to generate the route /users for the User index, and /users/:id (interpolated at runtime) for a single User. This, of course, can be completely overridden.

It is very much a struggle to not give you a grand tour of the entire API available in ReactiveRecord. Because it’s already in production use at Avail, it’s semi-refined to the developer’s every need. For being an opinionated framework, it does have a lot of built-in flexibility. So I’ll try to keep these examples simple and conceptual, unless of course, you’re thirsty for more!

At this point in our example, you could likely have this (over-simplified for instructional purposes only) file structure:

reactiverecord-example
├ /node_modules
├ store.js
└ Post.js

We’ve officially defined enough things for ReactiveRecord to be useful to us. We don’t really need the nice <Collection />, <Member /> and <Form /> components. Think of them as declarative React APIs for the ReactiveRecord core library.

To demo some of the core features of the Model, we’re going to play around with the Post model we just defined in the JavaScript console in the browser. Add the following line to your store.js file:

window.Post = ReactiveRecord.model("Post")

Once your build is loaded in the browser, you should be able to execute Post and see the model you defined. If you aren’t already using Redux dev tools, I highly recommend it now, because it will show you what is happening in the background with the generated reducer / generated actions in Redux. You won’t be writing them again. You won’t be using any of the Redux API for ReactiveRecord. It simply uses Redux as its own backend.

I’ve been a fan of Redux since I first learned about it. This was never meant to be a series of articles on how to cut it 💇🏻 out of your workflow. The more you learn about ReactiveRecord, I think you’ll see it’s one of the most sophisticated uses of Redux available, and I hope the Redux / React/ Rails communities see it that way, too. I don’t mean to sound self-congratulatory. It’s just I happen to know, having watched people’s faces as they begin using ReactiveRecord, that it’s more intuitive than what we’ve been doing. There will be many moments of “Oh wow, that just works!?” and “Duh! This is how it should have always worked!” Ok, sorry for being so chatty. Let’s learn more.

Listing Resources
The first action to try is a simple index of posts. Each of ReactiveRecord's remote-oriented methods return a Promise. Execute the following to retrieve a list of posts:

Post.all().then( posts => {
console.log(posts)
})

If you take a look at your network tab in the browser, you should see a GET request to http://jsonplaceholder.typicode.com/posts. Pretty simple.

Listing Resources (scoped by certain parameter)
It’s normal to scope API resources to the current user, but say we wanted to get posts only for the user with ID 1, we would need our endpoint to be http://jsonplaceholder.typicode.com/posts?userId=1. You can do this by passing a query (either query string or plain object) to .all() like so:

Post.all({ userId: 1 }).then( scopedPosts => {
console.log(scopedPosts)
})

This should retrieve only posts with that specific user ID. A couple things are also happening. Your Redux store now has each of these posts in the ReactiveRecord reducer. It’s available everywhere the store is available. Very neat. You don’t need to know what actions were dispatched to make this happen, or how it all works under the hood, but you can see for yourself using Redux dev tools. Let’s dive into the other methods available to us in the Model. You should play around with these methods and get comfortable using them in this example, and then create your own model. These will be easy to master, and then as our tutorials progress, you won’t need to write many of them anymore.

Remote Methods

Each of these methods return a promise, and can usually take a query . They generate GET requests only. APIs typically respond to these kinds of requests with the status 200 OK.

Model.find(primaryKey) => Promise
Retrieve a single API resource by its primary key, usually the ID. This will return the API resource as an instance of the defined model.

Post.find(12).then( post => { console.log(post) })

Model.all(optionalQuery) => Promise
Covered above, retrieve a collection of the defined model, returns a ReactiveRecordCollection, an Array of instances with some methods attached. The query can be either a query string ?status=published or a query object { status: "published" }.

Post.all().then( posts => { console.log(posts) })

Model.load() => Promise
Useful alias for .all() for singleton type models (models with one occurrence per browser session) , such as a CurrentUser model.

CurrentUser.load().then(currentUser => { console.log(currentUser) })

Model.prototype.reload() => Promise
A method that will reload the current instance in place (refreshing via a GET request).

post.reload().then( reloadedPost => { console.log(reloadedPost) })

Persistence Methods

These methods generate either a POST, PUT, or DELETE request to an API resource or endpoint. They are for persisting information, such as creating a post, or saving changes to a post in our example. Like the remote methods, they all return a promise.

Model.create({ ...attrs }) => Promise
Creates a resource based on attributes in the schema. Any attribute not in the schema will be ignored. You API should normally respond with the status 201 Created.

Post.create({
title: "My first post",
body: "The content of my first post is awesome!"
}).then( persistedPost => {
console.log(post.id, post)
})

Model.destroy(primaryKey) => Promise
Makes a DELETE request to an API resource, identified by a primary key, usually the ID attribute. Your API endpoint would usually respond with204 No Content or 202 Accepted.

Post.destroy(123).then( () => {})

Model.prototype.updateAttributes({ ...attrs }) => Promise
This method is shorthand for changing attributes directly on the model instance, then running Model.prototype.save(). It would invoke the setters attached to these attributes if defined.

myPost.updateAttributes({ title: "My New Post Title" }).then(
myPost => {
console.log(`Updated post title to: ${myPost.title}`)
}
)

Model.prototype.updateAttribute(attrName, attrValue) => Promise
This method is like .updateAttributes(), but for a single attribute.

myPost.updateAttribute("status", "published").then(
myPost => { console.log(`Post #${post.id} was published!`) }
)

Model.prototype.save() => Promise
Performs either a create or update action for an API resource, depending on whether the resource exists or not. Whether the resource exists is defined once, upon instantiating the resource (Difference between new Post(attrs, true) (persisted) and new Post(attrs) (not persisted). Your API should normally return the status 201 Created or 202 Accepted depending on if the resource exists. Calling save on any instance returned from the remote methods will result in an update type action being called. If the request is rejected, it will be available in the Promise catch, with an errors object as demonstrated below, the error messages already mapped to each attribute. More on errors later when we’re working with the <Form /> component. A rejected request is normally a 422 Unprocessable Entity response status in an API.

const myPost = new Post({ title: "My title", body:"My content" })
myPost.save().then( myPost => console.log("Post created!") )
Post.find(123).then( myPost => {
myPost.title = null
myPost.save().catch( myPost => console.log(e._errors) )
/* Would normally log an error message like "A title is required" */
})

Model.prototype.destroy() => Promise
This methods destroys an API resource in place. It is functionally the same as the static method Model.destroy().

myPost.destroy().then( ()=> console.log("I was destroyed!") )

Model Constructor

new Model(attrsObj, persistedBool) => instanceof Model
You can use pure JavaScript to initialize new model instances. New models take two arguments. The first argument are the attributes, which must be an Object. The last argument is a Boolean, whether or not ReactiveRecord should treat this instance as if it is persisted (.save() results in either a POST or PUT action).

const myPost = new Post({ title: "My title", body:"My content" })
const myPersistedPost = new Post(attrs, true)

Model Configuration

Models are highly configurable. The two most commonly configured components are the routes (if automatic route interpolation falls short) and the schema, which must be defined in order to manipulate attributes in the model instance. You configure your models way before you make any API requests.

Model.routes = {}
By default, ReactiveRecord will automatically generate routes based on the name of the model, and any prefix defined using the ReactiveRecord.setAPI instance method. Routes are broken down into five key actions performed as API requests. Those actions are index, create, show, update, and destroy. You can define which actions are allowed for each model, and which tokens should be used, if any, in the routes. Review the below configurations to get an idea of how to configure routes.

class Post extends Model {
static routes = {
only: "index"
}
}
class Post extends Model {
static routes = {
except: "destroy"
}
}
class Post extends Model {
static routes = {
only: ["show", "update"]
}
}
class Post extends Model {
static routes = {
except: ["update", "destroy"]
}
}
class Post extends Model {
static routes = {
index: ":prefix/all-my-posts",
show: ":prefix/:modelname/:title"
}
}

Route tokenization
Available tokens for route interpolation are:

  • :prefix the prefix defined using ReactiveRecord.setAPI
  • :modelname the pluralized name of the model
  • :id even if not manually defined in the schema. ID is the assumed primary key of each model unless otherwise stated.
  • :yourattributename any attribute name in the schema. It should really only be a String or Number, but can be literally anything. It will be encoded using encodeURIComponent to be placed in the final URL.
  • :anyattributename any attribute name not in the schema, but passed later as part of the query. ReactiveRecord will leave any unknown interpolation alone, but if given as part of the query, it will be interpolated. For example, if the update route is defined as :prefix/:modelname/:type and type is not in the schema, calling mypost.save({ query: { type: "breaking" } }) will send the request to http://.../posts/breaking. Very useful.

Model.schema = {}
The schema is where data types and default values are defined. Data types can currently be String, Number, Date, Boolean, Object, andArray. I want this soon be any kind of constructor, even other models. If your model’s primary key is id, omit this from the schema. It is the default primary key. You can always add a_primaryKey property to the schema to manually define a different primary key. Keep in mind this will change how the route is generated. As noted above, you can also add the _timestamps property to the schema, which will automatically add a read-only createdAt and updatedAt properties to the schema with the Date type. Even if your backend uses created_at snake case, you will use the createdAt method in ReactiveRecord. Omit createdAt and updatedAt from the schema if you are using _timestamps:true. Review the below configuration example to get an idea of how to configure the schema:

class Post extends Model {
static schema = {
title: String,
body: String,
published: Boolean,
category: {
default: "breaking-news",
type: String
},
published_at: Date,
userId: Number,
tags: Array,
_primaryKey: "slug",
_timestamps: true
}
}

Model.validations = {}
More on this later. You can manually define validations which will be used to validate forms in React, showing relevant errors on blur and change events, and map API error messages to the correct field visible in the UI. It’s possibly one of the most groundbreaking features of ReactiveRecord, so it definitely deserves its own separate article. More to come, pinky swear.

Model.store = {}
Your generated reducer will live here, but there is generally no need to change or access it. You could write your own reducer, but unless you have an intimate understanding of the way ReactiveRecord works, I wouldn’t recommend it just yet. The property you will want to define here is singleton, true or false (default false). The concept of singleton is useful in that it lets you define models that appear only once per client session, such as the current user, or Session, which would just contain login information. Once you write singleton: true, there will only ever be one of those models available across your entire single page application. CurrentUser is the best example to demonstrate this concept, because there is typically only ever one current user logged in, viewing information. See the below example for turning a model into singleton mode:

class CurrentUser extends Model {
static store = { singleton: true }
}

Bringing it all together
Though there are seemingly lots of options to configure a model, it really is quite simple for the majority of use cases. The model we defined earlier for the Post example is generally all that is needed, and doesn’t need any elaboration.

class Post extends Model {
static schema = {
title: String,
body: String,
_timestamps: true
}
}

The defaults are very sane, and lots of configuration stays out of your way until you need it, which keeps your code eloquent, readable and declarative, in my opinion.

In later tutorials, I’ll dive way deeper into model validations (when validating form fields), and talk about other utility methods on the model for checking if the instance is dirty / pristine, and what has changed, exactly.

Thanks so much for reading. Please let me know if you’re liking what you’re reading, and if you have anything to say. I’m very excited to know what people think. I’m not expecting a pat on the back by any means. I wrote ReactiveRecord for my team at Avail. To save us an immense amount of time and money. My goal now is for this to be incredibly useful to the developer at large, as it has been for us. If you’d like to be notified about Part 4, or continued articles related to ReactiveRecord, please send an e-mail to support@avail.com.

— Kyle Ramirez

--

--

Kyle Ramirez

Engineer at The Mom Project, pilot on airplane mode, former US Marine PAO, love building, storytelling and things that go fast