summaryrefslogtreecommitdiffstats
path: root/_posts/2015-07-20-A-practical-understanding-of-Flux.md
blob: 81d33a5fbe13f5346b1abebfaf2cd678a5dfee52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
---
# vim: tw=80
title: A practical understanding of Flux
layout: post
tags: [javascript, react]
---

[React.js](https://facebook.github.io/react/) and the
[Flux](https://facebook.github.io/flux/) are shaping up to be some of the most
important tools for web development in the coming years. The MVC model was
strong on the server when we decided to take the frontend seriously, and it was
shoehorned into the frontend since we didn't know any better. React and Flux
challenge that and I like where it's going very much. That being said, it was
very difficult for me to get into. I put together this blog post to serve as a
more *practical* guide - the upstream documentation tells you a lot of concepts
and expects you to put them together yourself. Hopefully at the end of this
blog post you can confidently start writing things with React+Flux instead of
reading brain-melting docs for a few hours like I did.

At the core of it, React and Flux are very simple and elegant. Far more simple
than the voodoo sales pitch upstream would have you believe. To be clear,
**React** is a framework-ish that lets you describe your UI through reusable
components, and includes *jsx* for describing HTML elements directly in your
JavaScript code. **Flux** is an *optional* architectural design philosophy that
you can adopt to help structure your applications. I have been using
[Babel](https://babeljs.io/) to compile my React+Flux work, which gives me
ES6/ES7 support - I strongly suggest you do the same. This blog post assumes
you're doing so. For a crash course on ES6, [read this entire
page](http://git.io/es6features). Crash course for ES7 is omitted here for
brevity, but [click this](https://gist.github.com/SirCmpwn/2e8e455c91494b7c3713)
if you're interested.

## Flux overview

Flux is based on a unidirectional data flow. The direction is: dispatcher ➜
stores ➜ views, and the data is actions. At the stores or views level, you can
give actions to the dispatcher, which passes them down the line.

Let's explain exactly what piece is, and how it fits in to your application.
After this I'll tell you some specific details and I have a starter kit prepared
for you to grab as well.

### Dispatcher

The dispatcher is very simple. Anything can register to receive a callback when
an "action" happens. There is one dispatcher and one set of callbacks, and
everything that registers for it will receive every action given to the
dispatcher, and can do with this as it pleases. Generally speaking you will only
have the stores listen to this. The kind of actions you will send along may look
something like this:

* Add a record
* Delete a record
* Fetch a record with a given ID
* Refresh a store

Anything that would change data is going to be given to the dispatcher and
passed along to the actions. Since everything receives every action you give to
the dispatcher, you have to encode something into each action that describes
what it's for. I use objects that look something like this:

{% highlight json %}
{
    "action": "STORE_NAME.ACTION_TYPE.ETC",
    ...
}
{% endhighlight %}

Where `...` is whatever extra data you need to include (the ID of the record
to fetch, the contents of the record to be added, the property that needs to
change, etc). Here's an example payload:

{% highlight json %}
{
    "action": "ACCOUNTS.CREATE.USER",
    "username": "SirCmpwn",
    "email": "sir@cmpwn.com",
    "password": "hunter2"
}
{% endhighlight %}

The Accounts store is listening for actions that start with `ACCOUNTS.` and when
it sees `CREATE.USER`, it knows a new user needs to be created with these
details.

### Stores

The stores just have ownership of data and handle any changes that happen to
that data. When the data changes, they raise events that the views can subscribe
to to let them know what's up. There's nothing magic going on here (I initially
thought there was magic). Here's a really simple store:

{% highlight javascript %}
import Dispatcher from "whatever";

export class UserStore {
    constructor() {
        this._users = [];
        this.action = this.action.bind(this);
        Dispatcher.register(this.action);
    }

    get Users() {
        return this._users;
    }

    action(payload) {
        switch (payload.action) {
        case "ACCOUNTS.CREATE.USER":
            this._users.push({ 
                "username": payload.username,
                "email": payload.email,
                "password": payload.password
            });
            raiseChangeEvent(); // Exercise for the reader
            break;
        }
    }
}

let store = new UserStore();
export default new UserStore();
{% endhighlight %}

Yeah, that's all there is to it. Each store should be a singleton. You use it
like this:

{% highlight javascript %}
import UserStore from "whatever/UserStore";

console.log(UserStore.Users);

UserStore.registerChangeEvent(() => {
    console.log(UserStore.Users); // This has changed now
});
{% endhighlight %}

Stores end up having a lot of boilerplate. I haven't quite figured out the best
way to address that yet.

### Views

Views are react components. What makes React components interesting is that they
re-render the whole thing when you call `setState`. If you want to change the
way it appears on the page for any reason, a call to `setState` will need to
happen. And here are the two circumstances under which they will change:

* In response to user input to change non-semantic view state
* In response to a change event from a store

The first bullet here means that you can call `setState` to change view states,
but not data. The second bullet is for when the data changes. When you change
view states, this refers to things like "click button to reveal form". When you
change data, this refers to things like "a new record was created, show it", or
even "a single property of a record changed, show that change".

**Wrong way**: you have a text box that updates the "name" of a record. When the
user presses the "Apply" key, the view will re-render itself with the new name.

**Right way**: When you press "Apply", the view sends an action to the
dispatcher to apply the change. The relevant store picks up the action, applies
the change to its own data store, and raises an event. Your view hears that
event and re-renders itself.

![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-1300w.png)

![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png)

### Why bother?

* Easy to have stores depend on each other
* All views that depend on the same stores are updated when it changes
* It follows that all cross-store dependencies are updated in a similar fashion
* Single source of truth for data
* Easy as pie to pick up and maintain with little knowledge of the codebase

## Practical problems

Here are some problems I ran into, and the fluxy solution to each.

### Need to load data async

You have a list of DNS records to show the user, but they're hanging out on the
server instead of in JavaScript objects. Here's how you accomodate for this:

* When you use a store, call `Store.fetchIfNecessary()` first.
* When you pull data from the store, expect `null` and handle this elegantly.
* When the initial fetch finishes in the store, raise a change event.

From `fetchIfNecessary` in the store, go do the request unless it's in progress or
done. On the view side, show a loading spinner or something if you get `null`.
When the change event happens, whatever code set the state of your component
initially will be re-run, and this time it won't get `null` - deal with it
appropriately (show the actual UI).

This works for more than things that are well-defined at dev time. If you need
to, for example, fetch data for an arbitrary ID:

* View calls `Store.userById(10)` and gets `null`, renders lack of data
    appropriately
* Store is like "my bad" and fetches it from the server
* Store raises change event when it arrives and the view re-renders

### Batteries not included

Upstream, in terms of actual usable code, flux just gives you a dispatcher. You
also need something to handle your events. This is easy to roll yourself, or you
can grab one of a bazillion things online that will do it for you. There is also
no base Store class for you, so make one of those. You should probably just
include some shared code for raising events and consuming actions. Mine looks
something like this:

{% highlight javascript %}
class UserStore extends Store {
    constructor() {
        super("USER");
        this._users = [];
        super.action("CREATE.USER", this.userCreated);
    }

    userCreated(payload) {
        this._users.push(...);
        super.raiseChangeEvent();
    }

    get Users {
        return this._users;
    }
}
{% endhighlight %}

Do what works best for you.

## Starter Kit

If you want something with the batteries in and a base to build from, I've got
you covered. Head over to
[SirCmpwn/react-starter-kit](https://github.com/SirCmpwn/react-starter-kit) on
Github.

## Conclusion

React and Flux are going to be big. This feels like the right way to build a
frontend. Hopefully I saved you from all the headache I went through trying to
"get" this stuff, and I hope it serves you well in the future. I'm going to be
pushing pretty hard for this model at my new gig, so I may be writing more blog
posts as I explore it in a large-scale application - stay tuned.