Frozen_state
Dec 1, 2015This is a followup to my previous post about React with a Flux-y store
Sidenote: I am looking seriously into Redux, but haven’t made the move yet.
One thing I really enjoy is using freezer to directly modify the local data and having freezer update itself and therefore all the views.
Another thing I noticed is that the when you need to persist the changes to the server, Reflux actions are a very nice way to define the change. However, it is a pain to have to define an action, create an endpoint, deal with the returned data, etc., each time you add a new action.
While React is the new-hotness on the client-side, for me, at least, EventSourcing and CQRS are the new-hotness (ok, not new but growing in popularity) for the server side. On reading about this, I have noticed a strong similarity between “Commands” on the server and “Actions” on the client side.
So, I was thinking, why not create a single endpoint on the server that accepts “actions” and converts to server “commands”?
Sidenote:
This is orthogonal to using something like GraphQL. It doesn’t exclude or replace its use but it doesn’t require it either. Adding GraphQL to this process would be quite easy.
If you look at the example from my previous post…
// recipe.jsx
var Actions = require('actions');
...
onClick: function (e) {
this.props.name.set("New Recipe Name");
// current way of calling store
Actions.changeName(this.props.id, "New Recipe Name");
}
}
module.exports = Recipe;
Previously, this would have made a call to a store method which then calls an explicit endpoint on the server specifically for changing the name, and then the store would have dealt with the data returned. This works but is time-consuming and repetitive.
You can now imagine that the Actions store could simply call an endpoint on the server with the method name and the data and all other “actions” would also hit the same endpoint. For simplicity, you might want to change the Actions call…
onClick: function (e) {
Actions.command("changeName", {id: this.props.id, name: "New Recipe Name");
}
The actions store would simply delegate to the endpoint.
...
onCommand: function (command, attrs) {
// fetch polyfill
fetch('/commands/' + command, attrs);
}
...
Once this is done, adding a new action would not require any changes to the store.
On the server side (Rails) you would need a CommandController
class CommandsController < ApplicationController
def create
if CommandRunner.run(command, params[:body])
render json: {}, status: :ok #for now
else
render json: {
error: ["something went wrong"]
}, status: 422
end
end
private
def command
params[:command]
end
end
If we wanted to get really fancy here, we could use something like RailsDisco for full event sourcing or use CommandObjects or use our own implementation for now.
class CommandRunner
def self.run(command, params)
runner = runners[command]
runner.new(params).run
end
private
def self.runners
{
changeName: NameChanger,
addIngredient: IngredientAdder,
}.with_indifferent_access
end
class Runner < Struct.new(:params)
end
# These would probably be in a different file
class NameChanger < Runner
def run
Recipe.find ...
etc ....
end
end
class IngredientAdder < Runner
def run
Recipe.find...
etc ...
end
end
end
SideNote: CommandObject by Josh Adams, a nicer implementation of Command Objects
class StudentTransferCommandsController < LoggedInController
def create
transfer = StudentTransferCommand.new(params[:student_transfer_command])
transfer.student_id = current_person.id
transfer.on_success = method(:on_success)
transfer.on_failure = method(:on_failure)
transfer.execute!
end
def on_success
flash[:success] = "Transfer successful."
redirect_to bank_path
end
def on_failure
flash[:error] = "Invalid transfer."
redirect_to bank_path
end
end
So we still have to deal with the response data from the server. What if we try to save but it fails for some reason?
Ideally, we would use something like Pusher to post back the data as it changes, but that’s a topic for another post. For now, we can have the store listen to the response with a callback.
... more code
onCommand: function (command, attrs) {
// using polyfill fetch
fetch('/commands/' + command, attrs)
.then(function(response) {
return response.json();
}).then(function(json) {
// Because we're using freezer, this will trigger Reflux to update
// This also implies that we are updating the entire tree of data
// which is not efficient.
// Ideally we would only replace the portion of tree that changed.
this.store.get().set(json);
}).catch(function(ex) {
console.log('deal with me!', ex);
})
;
}
... more code
The controller would of course have to return some data
class CommandsController < ApplicationController
def create
if id = CommandRunner.run(command, params[:body])
# all_data is pseudo-code of course
render json: all_data(id), status: :ok
...
end
We are making the assumption that the data returned from the server completely replaces the data on the client-side. While this might work for smaller apps, this is not great. We probably should return a subset of the changed data and replace/merge it with our existing data. We’ll leave that for another day as well.
Conclusion
So I think that by updating the data on the frontend with freezer and calling a “command” on the front-end, we have greatly simplified the front-end. Now the backend gets a command from the front-end (aka “action”) and it returns data after updating. Essentially, the store shouldn’t have to do much at all, just delegate to the server.
I think this is a nice clean way of dealing with changes. What do you think?
Related Posts
-
Chain of Responsibility for web requests
A technique to optionally handle web requests in an extendable way Read More
Jan 19, 2016 -
Plotting data for the web
Using the D3 library to create dynamic plots Read More
Dec 1, 2015 -
Frozen_state
Command your State - Using commands and actions to modify your state Read More
Dec 1, 2015