Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Framework-agnostic Web Applications with Redux

Framework-agnostic Web Applications with Redux

João Figueiredo

January 19, 2017
Tweet

More Decks by João Figueiredo

Other Decks in Programming

Transcript

  1. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data?
  2. const state = { counter: 0 }; const action1 =

    { type: 'INCREMENT' } const action2 = { type: 'DECREMENT' } const action3 = { type: 'INCREMENT_BY', payload: 3}
  3. const state = { counter: 0 }; const action1 =

    { type: 'INCREMENT' } const action2 = { type: 'DECREMENT' } const action3 = { type: 'INCREMENT_BY', payload: 3} function reducer(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 case 'INCREMENT_BY': return state + payload default: return state } }
  4. reducer(state, action); // newState reducer({ counter: 0 }, { type:

    ‘INCREMENT’ }); // { counter: 1 } reducer({ counter: 5 }, { type: ‘INCREMENT_BY', payload: 10 }); // { counter: 15 }
  5. import { createStore } from 'redux' let store = createStore(reducer)

    // initial state set to { counter: 0 } store.subscribe(() => console.log('new state is', store.getState()) )
  6. import { createStore } from 'redux' let store = createStore(reducer)

    // initial state set to { counter: 0 } store.subscribe(() => console.log('new state is', store.getState()) ) store.dispatch({ type: 'INCREMENT' }) // new state is { counter: 1 }
  7. import { createStore } from 'redux' let store = createStore(reducer)

    // initial state set to { counter: 0 } store.subscribe(() => console.log('new state is', store.getState()) ) store.dispatch({ type: 'INCREMENT' }) // new state is { counter: 1 } store.dispatch({ type: 'DECREMENT' }) // new state is { counter: 0 }
  8. import { createStore } from 'redux' let store = createStore(reducer)

    // initial state set to { counter: 0 } store.subscribe(() => console.log('new state is', store.getState()) ) store.dispatch({ type: 'INCREMENT' }) // new state is { counter: 1 } store.dispatch({ type: 'DECREMENT' }) // new state is { counter: 0 } store.dispatch({ type: ‘INCREMENT_BY', payload: 3}) // new state is { counter: 3 }
  9. import React from 'react' import { render } from 'react-dom'

    import { Provider } from 'react-redux' import { createStore } from 'redux' import reducer from './reducers'; import { MyComponent } from './components/MyComponent'; const store = createStore(reducer); render( <Provider store={store}> <MyComponent /> </Provider>, document.getElementById('root') )
  10. import React from 'react' import { render } from 'react-dom'

    import { Provider } from 'react-redux' import { createStore } from 'redux' import reducer from './reducers'; import { MyComponent } from './components/MyComponent'; const store = createStore(reducer); render( <Provider store={store}> <MyComponent /> </Provider>, document.getElementById('root') ) import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import reducer from ‘./reducers'; import { MyComponent } from ‘./components/MyComponent'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, StoreModule.provideStore(reducer) ], bootstrap: [MyComponent] }) export class AppModule { }
  11. import React from 'react' import { render } from 'react-dom'

    import { Provider } from 'react-redux' import { createStore } from 'redux' import reducer from './reducers'; import { MyComponent } from './components/MyComponent'; const store = createStore(reducer); render( <Provider store={store}> <MyComponent /> </Provider>, document.getElementById('root') ) import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import reducer from ‘./reducers'; import { MyComponent } from ‘./components/MyComponent'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, StoreModule.provideStore(reducer) ], bootstrap: [MyComponent] }) export class AppModule { } ⛔
  12. import React from 'react' import { render } from 'react-dom'

    import { Provider } from 'react-redux' import { createStore } from 'redux' import reducer from './reducers'; import { MyComponent } from './components/MyComponent'; const store = createStore(reducer); render( <Provider store={store}> <MyComponent /> </Provider>, document.getElementById('root') ) import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import reducer from ‘./reducers'; import { MyComponent } from ‘./components/MyComponent'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, StoreModule.provideStore(reducer) ], bootstrap: [MyComponent] }) export class AppModule { } ✅
  13. import React from 'react' import { render } from 'react-dom'

    import { Provider } from 'react-redux' import { createStore } from 'redux' import reducer from './reducers'; import { MyComponent } from './components/MyComponent'; const store = createStore(reducer); render( <Provider store={store}> <MyComponent /> </Provider>, document.getElementById('root') ) import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import reducer from ‘./reducers'; import { MyComponent } from ‘./components/MyComponent'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, StoreModule.provideStore(reducer) ], bootstrap: [MyComponent] }) export class AppModule { }
  14. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
  15. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } }
  16. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } } ⛔
  17. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } } ⛔
  18. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } } ⛔
  19. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } } ✅
  20. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } }
  21. import React, { Component } from 'react' import { connect

    } from 'react-redux' import * as actions from '../actions' export class MyComponent extends Component { render() { return <span className="counter">counter is: { this.props.counter }</span> } } const mapStateToProps = (state, ownProps) => ({ counter: state.counter }) export default connect(mapStateToProps, mapDispatchToProps)(MyComponent) import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import 'rxjs/add/operator/map'; import * as actions from '../actions'; @Component({ selector: 'my-component', template: `<span class="counter">counter is: {{ counter }}</span>` }) export class MyComponent { constructor(store: Store<any>){ this.store = store; this.actions = actions; store.subscribe(state => this.counter = state.counter); } } ✅
  22. // state de-normalised as a dictionary (e.g. json api ==>

    firebase) const kanbanState = { columns: { "47749e5": { name: “Todo” , index: 0 }, "dde4872": { name: “Doing", index: 1 }, "dde4872": { name: “Done” , index: 2 } } }
  23. // state de-normalised as a dictionary (e.g. json api ==>

    firebase) const kanbanState = { columns: { "47749e5": { name: “Todo” , index: 0 }, "dde4872": { name: “Doing", index: 1 }, "dde4872": { name: “Done” , index: 2 } } } // selectors: normalise data so it’s easier to display it later. import { sortBy } from 'lodash'; const arrayfyObject = (obj) => Object.keys(obj).map(id => ({ ...obj[id], id })) export function getSortedColumns(state) { const columns = arrayfyObject(state.columns || {}); return sortBy(columns, 'index'); }
  24. const kanbanState = { columns: { "47749e5": { name: “Todo”

    , index: 0 }, "dde4872": { name: “Doing", index: 1 }, "dde4872": { name: “Done” , index: 2 } } } getSortedColumns(kanbanState); // [ // { id: "47749e5", name: "Todo" , index: 0 }, // { id: "dde4872", name: "Doing", index: 1 }, // { id: "dde4872", name: "Done" , index: 2 } // ]
  25. // actions.js export const RENAME_COLUMUN = 'RENAME_COLUMUN' export const renameColumn

    = (id, name) => ({ type: RENAME_COLUMUN, payload: {id, name} })
  26. // actions.js export const RENAME_COLUMUN = 'RENAME_COLUMUN' export const renameColumn

    = (id, name) => ({ type: RENAME_COLUMUN, payload: {id, name} }) // reducers.js export default function columns (state = {}, { type, payload }) { if (type === RENAME_COLUMUN) { return Object.assign({}, state, { [payload.id]: Object.assign({}, state[payload.id], { name: payload.name }) }); } return state; } // selectors go usually here
  27. columns(kanbanState, renameColumn("ac32fcc", "Shipped")); // newKanbanState: { // columns: { //

    "47749e5": { // "name": "Todo", // "index": 0 // }, // "dde4872": { // "name": "Doing", // "index": 1 // }, // "ac32fcc": { // "name": "Shipped", // "index": 2 // } // } // }
  28. Presentational Components Container Components concern how things look how things

    work Presentational and Container Components by Dan Abramov
  29. Presentational Components Container Components concern how things look how things

    work dependencies — actions and store Presentational and Container Components by Dan Abramov
  30. Presentational Components Container Components concern how things look how things

    work dependencies — actions and store logic — data loading and mutation Presentational and Container Components by Dan Abramov
  31. Presentational Components Container Components concern how things look how things

    work dependencies — actions and store logic — data loading and mutation state stateless (pure) stateful Presentational and Container Components by Dan Abramov
  32. Presentational Components Container Components concern how things look how things

    work dependencies — actions and store logic — data loading and mutation state stateless (pure) stateful Presentational and Container Components by Dan Abramov visual interaction
  33. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div>
  34. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div> @Component({ selector: 'column', template: ` <div> <span>{{ column.name }}</span> <button (click)="onAddTask.emit()"> Add Task </button> </div>` }) export class ColumnComponent { @Input() column; @Output() onAddTask = new EventEmitter(); }
  35. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div> @Component({ selector: 'column', template: ` <div> <span>{{ column.name }}</span> <button (click)="onAddTask.emit()"> Add Task </button> </div>` }) export class ColumnComponent { @Input() column; @Output() onAddTask = new EventEmitter(); }
  36. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div> @Component({ selector: 'column', template: ` <div> <span>{{ column.name }}</span> <button (click)="onAddTask.emit()"> Add Task </button> </div>` }) export class ColumnComponent { @Input() column; @Output() onAddTask = new EventEmitter(); }
  37. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div> @Component({ selector: 'column', template: ` <div> <span>{{ column.name }}</span> <button (click)="onAddTask.emit()"> Add Task </button> </div>` }) export class ColumnComponent { @Input() column; @Output() onAddTask = new EventEmitter(); }
  38. export default const Column = ({ id, name, onAddTask })

    => <div> <span>{name}</span> <button onClick={onAddTask}> Add Task </button> </div> @Component({ selector: 'column', template: ` <div> <span>{{ column.name }}</span> <button (click)="onAddTask.emit()"> Add Task </button> </div>` }) export class ColumnComponent { @Input() column; @Output() onAddTask = new EventEmitter(); }
  39. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }

  40. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }
 import { Store } from '@ngrx/store'; import * as actions from '../actions'; import { getColumnsWithTasks } from '../reducers'; @Component({ selector: 'app-board', template: ` <div> <column *ngFor="let column of columns" [column]="column" (onAddTask)="this.store.dispatch( actions.addTask(column.id) )" /> </div>` }) export class Board { constructor(store: Store<any>){ this.actions = actions; this.store = store; store.subscribe(state => this.columns = getColumnsWithTasks(state) ); } }
  41. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }
 import { Store } from '@ngrx/store'; import * as actions from '../actions'; import { getColumnsWithTasks } from '../reducers'; @Component({ selector: 'app-board', template: ` <div> <column *ngFor="let column of columns" [column]="column" (onAddTask)="this.store.dispatch( actions.addTask(column.id) )" /> </div>` }) export class Board { constructor(store: Store<any>){ this.actions = actions; this.store = store; store.subscribe(state => this.columns = getColumnsWithTasks(state) ); } }
  42. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }
 import { Store } from '@ngrx/store'; import * as actions from '../actions'; import { getColumnsWithTasks } from '../reducers'; @Component({ selector: 'app-board', template: ` <div> <column *ngFor="let column of columns" [column]="column" (onAddTask)="this.store.dispatch( actions.addTask(column.id) )" /> </div>` }) export class Board { constructor(store: Store<any>){ this.actions = actions; this.store = store; store.subscribe(state => this.columns = getColumnsWithTasks(state) ); } }
  43. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }
 import { Store } from '@ngrx/store'; import * as actions from '../actions'; import { getColumnsWithTasks } from '../reducers'; @Component({ selector: 'app-board', template: ` <div> <column *ngFor="let column of columns" [column]="column" (onAddTask)="this.store.dispatch( actions.addTask(column.id) )" /> </div>` }) export class Board { constructor(store: Store<any>){ this.actions = actions; this.store = store; store.subscribe(state => this.columns = getColumnsWithTasks(state) ); } }
  44. import { connect } from 'react-redux' import * as actions

    from "../actions" import { getColumnsWithTasks } from "../reducers" @connect( (state) => ({ columns: getColumnsWithTasks(state) }), actions ) class Board extends Component { render() { const { addTask, columns } = this.props; return ( <div> { columns.map(column => <Column {...column} onAddTask={ () => addTask(column.id) } /> )} </div> ) } }
 import { Store } from '@ngrx/store'; import * as actions from '../actions'; import { getColumnsWithTasks } from '../reducers'; @Component({ selector: 'app-board', template: ` <div> <column *ngFor="let column of columns" [column]="column" (onAddTask)="this.store.dispatch( actions.addTask(column.id) )" /> </div>` }) export class Board { constructor(store: Store<any>){ this.actions = actions; this.store = store; store.subscribe(state => this.columns = getColumnsWithTasks(state) ); } }
  45. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data?
  46. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ⛔
  47. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ⛔
  48. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ✅ ⛔
  49. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ✅ ✅ ⛔
  50. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ✅ ✅ ✅ ⛔
  51. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ✅ ✅ ⛔ ✅ ✅
  52. views how does it look application logic what happens if

    I click here business logic how do we change data Framework-agnostic Webapps routes what pages do we have, when do we navigate between pages. talking to a server get, store and update data. data validation what is valid data? what is invalid data? ✅ ✅ ✅ ✅ ⛔
  53. The exploration goes on… Implement a Vue.js kanban (through Vuex)

    Add asynchronous actions (e.g.: store on firebase) Add routes (create a timeline view) Implement an Aurelia kanban (through aurelia-redux)
  54. If you want to dig more: Getting Started with Redux

    Course by Dan Abramov Building React Applications with Idiomatic Redux by Dan Abramov Build Redux Applications with Angular2, RxJS, and ngrx/store by John Lindquist Vuex — Vue.js State Management Pattern (inspired by redux)