logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

A-practical-understanding-of-Flux.md (9278B)


  1. ---
  2. date: 2015-07-20
  3. # vim: tw=80
  4. title: A practical understanding of Flux
  5. layout: post
  6. tags: [javascript, react]
  7. ---
  8. [React.js](https://facebook.github.io/react/) and the
  9. [Flux](https://facebook.github.io/flux/) are shaping up to be some of the most
  10. important tools for web development in the coming years. The MVC model was
  11. strong on the server when we decided to take the frontend seriously, and it was
  12. shoehorned into the frontend since we didn't know any better. React and Flux
  13. challenge that and I like where it's going very much. That being said, it was
  14. very difficult for me to get into. I put together this blog post to serve as a
  15. more *practical* guide - the upstream documentation tells you a lot of concepts
  16. and expects you to put them together yourself. Hopefully at the end of this
  17. blog post you can confidently start writing things with React+Flux instead of
  18. reading brain-melting docs for a few hours like I did.
  19. At the core of it, React and Flux are very simple and elegant. Far more simple
  20. than the voodoo sales pitch upstream would have you believe. To be clear,
  21. **React** is a framework-ish that lets you describe your UI through reusable
  22. components, and includes *jsx* for describing HTML elements directly in your
  23. JavaScript code. **Flux** is an *optional* architectural design philosophy that
  24. you can adopt to help structure your applications. I have been using
  25. [Babel](https://babeljs.io/) to compile my React+Flux work, which gives me
  26. ES6/ES7 support - I strongly suggest you do the same. This blog post assumes
  27. you're doing so. For a crash course on ES6, [read this entire
  28. page](http://git.io/es6features). Crash course for ES7 is omitted here for
  29. brevity, but [click this](https://gist.github.com/SirCmpwn/2e8e455c91494b7c3713)
  30. if you're interested.
  31. ## Flux overview
  32. Flux is based on a unidirectional data flow. The direction is: dispatcher ➜
  33. stores ➜ views, and the data is actions. At the stores or views level, you can
  34. give actions to the dispatcher, which passes them down the line.
  35. Let's explain exactly what piece is, and how it fits in to your application.
  36. After this I'll tell you some specific details and I have a starter kit prepared
  37. for you to grab as well.
  38. ### Dispatcher
  39. The dispatcher is very simple. Anything can register to receive a callback when
  40. an "action" happens. There is one dispatcher and one set of callbacks, and
  41. everything that registers for it will receive every action given to the
  42. dispatcher, and can do with this as it pleases. Generally speaking you will only
  43. have the stores listen to this. The kind of actions you will send along may look
  44. something like this:
  45. * Add a record
  46. * Delete a record
  47. * Fetch a record with a given ID
  48. * Refresh a store
  49. Anything that would change data is going to be given to the dispatcher and
  50. passed along to the actions. Since everything receives every action you give to
  51. the dispatcher, you have to encode something into each action that describes
  52. what it's for. I use objects that look something like this:
  53. ```json
  54. {
  55. "action": "STORE_NAME.ACTION_TYPE.ETC",
  56. ...
  57. }
  58. ```
  59. Where `...` is whatever extra data you need to include (the ID of the record
  60. to fetch, the contents of the record to be added, the property that needs to
  61. change, etc). Here's an example payload:
  62. ```json
  63. {
  64. "action": "ACCOUNTS.CREATE.USER",
  65. "username": "SirCmpwn",
  66. "email": "sir@cmpwn.com",
  67. "password": "hunter2"
  68. }
  69. ```
  70. The Accounts store is listening for actions that start with `ACCOUNTS.` and when
  71. it sees `CREATE.USER`, it knows a new user needs to be created with these
  72. details.
  73. ### Stores
  74. The stores just have ownership of data and handle any changes that happen to
  75. that data. When the data changes, they raise events that the views can subscribe
  76. to to let them know what's up. There's nothing magic going on here (I initially
  77. thought there was magic). Here's a really simple store:
  78. ```js
  79. import Dispatcher from "whatever";
  80. export class UserStore {
  81. constructor() {
  82. this._users = [];
  83. this.action = this.action.bind(this);
  84. Dispatcher.register(this.action);
  85. }
  86. get Users() {
  87. return this._users;
  88. }
  89. action(payload) {
  90. switch (payload.action) {
  91. case "ACCOUNTS.CREATE.USER":
  92. this._users.push({
  93. "username": payload.username,
  94. "email": payload.email,
  95. "password": payload.password
  96. });
  97. raiseChangeEvent(); // Exercise for the reader
  98. break;
  99. }
  100. }
  101. }
  102. let store = new UserStore();
  103. export default new UserStore();
  104. ```
  105. Yeah, that's all there is to it. Each store should be a singleton. You use it
  106. like this:
  107. ```js
  108. import UserStore from "whatever/UserStore";
  109. console.log(UserStore.Users);
  110. UserStore.registerChangeEvent(() => {
  111. console.log(UserStore.Users); // This has changed now
  112. });
  113. ```
  114. Stores end up having a lot of boilerplate. I haven't quite figured out the best
  115. way to address that yet.
  116. ### Views
  117. Views are react components. What makes React components interesting is that they
  118. re-render the whole thing when you call `setState`. If you want to change the
  119. way it appears on the page for any reason, a call to `setState` will need to
  120. happen. And here are the two circumstances under which they will change:
  121. * In response to user input to change non-semantic view state
  122. * In response to a change event from a store
  123. The first bullet here means that you can call `setState` to change view states,
  124. but not data. The second bullet is for when the data changes. When you change
  125. view states, this refers to things like "click button to reveal form". When you
  126. change data, this refers to things like "a new record was created, show it", or
  127. even "a single property of a record changed, show that change".
  128. **Wrong way**: you have a text box that updates the "name" of a record. When the
  129. user presses the "Apply" key, the view will re-render itself with the new name.
  130. **Right way**: When you press "Apply", the view sends an action to the
  131. dispatcher to apply the change. The relevant store picks up the action, applies
  132. the change to its own data store, and raises an event. Your view hears that
  133. event and re-renders itself.
  134. ![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-1300w.png)
  135. ![](https://facebook.github.io/flux/img/flux-simple-f8-diagram-with-client-action-1300w.png)
  136. ### Why bother?
  137. * Easy to have stores depend on each other
  138. * All views that depend on the same stores are updated when it changes
  139. * It follows that all cross-store dependencies are updated in a similar fashion
  140. * Single source of truth for data
  141. * Easy as pie to pick up and maintain with little knowledge of the codebase
  142. ## Practical problems
  143. Here are some problems I ran into, and the fluxy solution to each.
  144. ### Need to load data async
  145. You have a list of DNS records to show the user, but they're hanging out on the
  146. server instead of in JavaScript objects. Here's how you accomodate for this:
  147. * When you use a store, call `Store.fetchIfNecessary()` first.
  148. * When you pull data from the store, expect `null` and handle this elegantly.
  149. * When the initial fetch finishes in the store, raise a change event.
  150. From `fetchIfNecessary` in the store, go do the request unless it's in progress or
  151. done. On the view side, show a loading spinner or something if you get `null`.
  152. When the change event happens, whatever code set the state of your component
  153. initially will be re-run, and this time it won't get `null` - deal with it
  154. appropriately (show the actual UI).
  155. This works for more than things that are well-defined at dev time. If you need
  156. to, for example, fetch data for an arbitrary ID:
  157. * View calls `Store.userById(10)` and gets `null`, renders lack of data
  158. appropriately
  159. * Store is like "my bad" and fetches it from the server
  160. * Store raises change event when it arrives and the view re-renders
  161. ### Batteries not included
  162. Upstream, in terms of actual usable code, flux just gives you a dispatcher. You
  163. also need something to handle your events. This is easy to roll yourself, or you
  164. can grab one of a bazillion things online that will do it for you. There is also
  165. no base Store class for you, so make one of those. You should probably just
  166. include some shared code for raising events and consuming actions. Mine looks
  167. something like this:
  168. ```js
  169. class UserStore extends Store {
  170. constructor() {
  171. super("USER");
  172. this._users = [];
  173. super.action("CREATE.USER", this.userCreated);
  174. }
  175. userCreated(payload) {
  176. this._users.push(...);
  177. super.raiseChangeEvent();
  178. }
  179. get Users {
  180. return this._users;
  181. }
  182. }
  183. ```
  184. Do what works best for you.
  185. ## Starter Kit
  186. If you want something with the batteries in and a base to build from, I've got
  187. you covered. Head over to
  188. [SirCmpwn/react-starter-kit](https://github.com/SirCmpwn/react-starter-kit) on
  189. Github.
  190. ## Conclusion
  191. React and Flux are going to be big. This feels like the right way to build a
  192. frontend. Hopefully I saved you from all the headache I went through trying to
  193. "get" this stuff, and I hope it serves you well in the future. I'm going to be
  194. pushing pretty hard for this model at my new gig, so I may be writing more blog
  195. posts as I explore it in a large-scale application - stay tuned.