Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Awesome State Management for React and Other Virtual-Dom Libraries

228 views

Published on

Presented at FITC's Web Unleashed 2017 conference
More info at www.fitc.ca/webu

Fred Daoud
Modernizing Medicine
Overview

Do you use React or any other virtual-DOM-based library that you love, but wonder about how to structure and manage application state? Perhaps you’ve looked at Redux, but got bogged down in boilerplate and action constants. Then you wondered about asynchronous actions. Does the list of libraries and dependencies that you need to solve common problems just keep piling up? Wasn’t React supposed to be a simple view library?

Let’s talk about other ways to manage the data flow of your web applications. We want a single state object, one-directional data flow, reactive views as plain functions, decoupled and reusable components, routing, time-traveling, …all without any fuss. We’ll assemble this in a way that lets you keep using your favourite libraries (React, Preact, Inferno, Snabbdom, Mithril, etc.), and write your code with regular JavaScript functions, giving you state management with the freedom to structure your application as best suits your requirements.

Objective

Show developers great ways to manage the state of applications using React or any virtual-DOM library.

Target Audience

Front-end JavaScript developers

Assumed Audience Knowledge

JavaScript and basic React or other virtual dom library.

Five Things Audience Members Will Learn

Managing application state with a single model and unidirectional flow
Creating decoupled, reusable components (with plain objects and functions!)
Assembling and composing with simple functional programming techniques
Routing made simple, using the library of your choice
Time-travelling and other cool development tools

Published in: Internet
  • Be the first to comment

  • Be the first to like this

Awesome State Management for React and Other Virtual-Dom Libraries

  1. 1. AWESOME STATEMANAGEMENT FORREACT* *ANDOTHERVIRTUAL-DOMLIBRARIES Fred Daoud - @foxdonut00
  2. 2. WHYAWESOME? NOTALIBRARY.ASIMPLEPATTERN.
  3. 3. WORKSWITHANY VIRTUALDOMLIBRARY
  4. 4. VIRTUALDOMISNICE VIEW = FUNCTION(MODEL)
  5. 5. “HOWDOI MANAGESTATE?”
  6. 6. REDUX MOBX CEREBRAL CYCLE.JS
  7. 7. LET'SLOOKAT ADIFFERENTAPPROACH.
  8. 8. MEIOSIS HTTP://MEIOSIS.JS.ORG
  9. 9. THEMEIOSISPATTERN model ← single source of truth view = function(model) update model → view is automatically re-rendered
  10. 10. model = single source of truth view = function(model) const initialModel = { counter: 0 }; const view = model => ( <div>Counter is {model.counter}</div> );
  11. 11. actions update the model const createActions = update => ({ increment: () => update(model => { model.counter++; return model; }) // after calling update(...), // view is automatically re-rendered });
  12. 12. views call actions const createView = actions => model => ( <div> <div>Counter is {model.counter}</div> <button onClick={actions.increment}>Increment</button> </div> );
  13. 13. WHATISUPDATE? HOWDOWEAUTOMATICALLY RE-RENDERTHEVIEW?
  14. 14. MEIOSISPATTERNIMPLEMENTATION Minimal streams: just map and scan Or, write your own minimal implementation
  15. 15. FLYDSTREAMS MAP const s1 = flyd.stream(); const s2 = s1.map(value => value * 10); s2.map(value => console.log(value)); s1(5); s1(10); // console output is 50 100
  16. 16. FLYDSTREAMS SCAN const s1 = flyd.stream(); const add = (x, y) => x + y; const s2 = flyd.scan(add, 0, s1); s2.map(value => console.log(value)); s1(5); s1(13); s1(24); // console output is 0 5 18 42
  17. 17. THEMEIOSISPATTERN const initialModel = { counter: 0 }; const update = flyd.stream(); const applyUpdate = (model, modelUpdate) => modelUpdate(model); const models = flyd.scan(applyUpdate, initialModel, update); const view = createView(createActions(update)); const element = document.getElementById("app"); models.map(model => ReactDOM.render(view(model), element));
  18. 18. USINGDIFFERENTVIRTUALDOMLIBS models.map(model => ReactDOM.render(view(model), element)); models.map(model => Inferno.render(view(model), element)); models.map(model => m.render(element, view(model))); models.map(model => preact.render(view(model), element, element.lastElementChild)); const render = view => element = patch(element, view); models.map(model => render(view(model)));
  19. 19. MODELUPDATE PLAIN update(model => { model.counter++; return model; });
  20. 20. LODASH LODASHFP RAMDA IMMUTABLE.JS update(model => _.update(model, "counter", _.partial(_.add,1))); update(_.update("counter", _.add(1))); update(R.over(R.lensProp("counter"), R.add(1))); update(model => model.update("counter", v => v + 1));
  21. 21. COMPONENTS
  22. 22. ACOMPONENTISJUST ANOBJECTWITHFUNCTIONS import { createActions } from "./actions"; import { createView } from "./view"; // This is the same 'update' stream ↓↓ export const createTemperature = update => ({ model: () => ({ date: "", value: 20, units: "C" }), view: createView(createActions(update)) });
  23. 23. TEMPERATUREEXAMPLE
  24. 24. ACTIONS(1/2) export const createActions = update => ({ editDate: evt => update(model => { model.date = evt.target.value; return model; }), increase: amount => () => update(model => { model.value = model.value + amount; return model; }),
  25. 25. ACTIONS(2/2) changeUnits: () => update(model => { if (model.units === "C") { model.units = "F"; model.value = Math.round( model.value * 9 / 5 + 32 ); } else { model.units = "C"; model.value = Math.round( (model.value - 32) / 9 * 5 ); } return model; }) });
  26. 26. VIEW export const createView = actions => model => ( <div> <div>Date: <input type="text" size="10" value={model.date} onChange={actions.editDate}/></div> <span>Temperature: {model.value}&deg;{model.units} </span> <div> <button onClick={actions.increase(1)}>Increase</button> <button onClick={actions.increase(-1)}>Decrease</button> </div> <div> <button onClick={actions.changeUnits}>Units</button> </div> </div> );
  27. 27. THEMEIOSISPATTERN const update = flyd.stream(); const temperature = createTemperature(update); // <-------- const initialModel = temperature.model(); // <-------- const applyUpdate = (model, modelUpdate) => modelUpdate(model); const models = flyd.scan(applyUpdate, initialModel, update); const element = document.getElementById("app"); models.map(model => ReactDOM.render(temperature.view(model), // <-------- element));
  28. 28. MEIOSISTRACER TIME-TRAVELDEVTOOL
  29. 29. MEIOSISTRACERINCHROMEDEVTOOLS
  30. 30. MEIOSISTRACERINPAGE
  31. 31. USINGMEIOSISTRACERINCHROME Install meiosis as a dev dependency Install the Chrome extension // Only for using Meiosis Tracer in development. import { trace } from "meiosis"; trace({ update, dataStreams: [ models ] });
  32. 32. USINGMEIOSISTRACERINPAGE Also install meiosis-tracer as a dev dep. <div id="tracer"></div> // Only for using Meiosis Tracer in development. import { trace } from "meiosis"; import meiosisTracer from "meiosis-tracer"; trace({ update, dataStreams: [ models ] }); meiosisTracer({ selector: "#tracer" });
  33. 33. REUSABLECOMPONENTS
  34. 34. REUSINGTHETEMPERATURECOMPONENT export const createApp = update => { const components = { air: createTemperature(nest(update, "air")), water: createTemperature(nest(update, "water")) }; return { model: () => ({ air: components.air.model(), water: components.water.model() }), view: createView(components) }; };
  35. 35. NESTINGTHEUPDATEFUNCTION export const nest = (update, path) => modelUpdate => update(model => { model[path] = modelUpdate(model[path]); return model; });
  36. 36. THEVIEW export const createView = components => model => ( <div> <h4>App</h4> {components.air.view(model.air)} {components.water.view(model.water)} </div> );
  37. 37. REUSINGTHETEMPERATURECOMPONENT
  38. 38. PATHREPETITION const components = { air: createTemperature(nest(update, "air")), water: createTemperature(nest(update, "water")) }; model: () => ({ air: components.air.model(), water: components.air.model() }) <div> {components.air.view(model.air)} {components.water.view(model)} </div>
  39. 39. ELIMINATINGPATHREPETITION const components = createComponents(update, { air: createTemperature, // passes nest(update, "air") water: createTemperature // also wraps view(model["water"]) }); return { // returns { air: c.air.model(), water: c.water.model() } model: combineComponents(components, "model"), view: createView(components) }; <div> {components.air.view(model)} {components.water.view(model)} </div>
  40. 40. COMPUTEDPROPERTIES
  41. 41. COMPUTEDPROPERTIES(1/2) export const createTemperature = update => { const computed = model => { let temp = model.value; if (model.units === "F") { temp = Math.round((temp - 32) * 5 / 9); } model.comment = (temp < 10) ? "COLD!" : (temp > 40) ? "HOT" : ""; return model; };
  42. 42. COMPUTEDPROPERTIES(2/2) const view = createView(createActions(update)); return { model: () => ({ date: "", value: 20, units: "C" }), view: model => view(computed(model)) // view: R.compose(view, computed) }; };
  43. 43. COMPUTEDPROPERTIES
  44. 44. ROUTING
  45. 45. ROUTINGEXAMPLE
  46. 46. ROUTING:PAGES export const pages = { home: { id: "Home", tab: "Home" }, coffee: { id: "Coffee", tab: "Coffee" }, beer: { id: "Beer", tab: "Beer" }, beerDetails: { id: "BeerDetails", tab: "Beer" } };
  47. 47. ROUTING:NAVIGATION export const createNavigation = update => { const navigate = (page, params = {}) => update(model => Object.assign(model, ({ page, params }))); const navigateToBeer = () => { services.loadBeer().then(beerList => { update(model => Object.assign(model, { beerList })); navigate(pages.beer); }); }; return { navigateToHome, navigateToBeer, ... }; };
  48. 48. ROUTING:APP export const createApp = (update, navigation) => { const homeComponent = createHome(update); //more... const pageMap = { [pages.home.id]: homeComponent, //more... }; return { view: model => { const component = pageMap[model.page.id]; return ( // render tabs, model.page.tab determines active tab {component.view(model)} ); } }; };
  49. 49. ROUTING:ROUTES export const createRouter = navigation => { const routes = { "/": { id: pages.home.id, action: navigation.navigateToHome }, "/coffee/:id?": { id: pages.coffee.id, action: navigation.navigateToCoffee }, "/beer": { id: pages.beer.id, action: navigation.navigateToBeer }, "/beer/:id": { id: pages.beerDetails.id, action: navigation.navigateToBeerDetails } };
  50. 50. ROUTING:RESOLVEROUTE import Mapper from "url-mapper"; const resolveRoute = () => { const route = document.location.hash.substring(1); const resolved = urlMapper.map(route, routes); if (resolved) { resolved.match.action(resolved.values); } }; window.onpopstate = resolveRoute;
  51. 51. ROUTING:ROUTESYNC const routeMap = Object.keys(routes).reduce((result, route) => { result[routes[route].id] = route; return result; }, {}); const routeSync = model => { const segment = routeMap[model.page.id] || "/"; const route = urlMapper.stringify(segment, model.params||{}); if (document.location.hash.substring(1) !== route) { window.history.pushState({}, "", "#" + route); } };
  52. 52. ROUTINGEXAMPLE
  53. 53. MEIOSIS Documentation • Examples Tracer • Gitter chat Fred Daoud • @foxdonut00 HTTP://MEIOSIS.JS.ORG

×