A Lesson in Flux

no I promise it is not that scary

What  is Flux

  • An application architecture
    • think MVC
  • NOT a framework
  • focus on data production/consumption
  • Often paired with React.js

An aside -MVC

  • Interactions can be complex
  • Hard to trace and debug
  • Changes can cascade
  • linear code base to complexity growth

Flux flow

  • All data flows through dispatcher
  • stores update themselves
  • Views pass down store data to children
  • In general flow is
    • view sends action
    • dispatcher tells store  
    • store updates
    • view listens for changes in store
  • one dispatcher vs many controllers

Flux details

  • Dispatcher
  • stores
  • views
  • controller views

Dispatcher

  • manages all data flow
  • accepts actions and dispatches the data to stores
  • stores register to dispatcher via callback
  • is very dumb

Stores

  • contain app state and logic
  • receives {ACTION, ACTION_DATA} 
  • Determines how to update state from action
  • broadcast events to views when changed

Views

  • Present data from stores
  • listen to store changes
  • React used here
  • Create actions to send to dispatcher
  • Controller Views
    • Views that only listen for store changes
    • pass store data changes to children
    • no UI here
  • Non Controller Views
    • render UI from data passed to them

No more theory!

  • Flux is just an architecture
  • Many implementations of Flux
    • Redux
    • Alt
    • Fluxxor

Alt Actions

var alt = require('alt');

class LocationActions {
  updateLocations(locations) {
    return locations;
  }

fetchLocations() {
  return (dispatch) => {
    // we dispatch an event here so we can have "loading" state.
    dispatch();
    LocationSource.fetch()
      .then((locations) => {
        // we can access other actions within our action through `this.actions`
        this.updateLocations(locations);
      })
      .catch((errorMessage) => {
        this.locationsFailed(errorMessage);
      });
    }
}
locationsFailed(errorMessage) {
  return errorMessage;
 }
}

module.exports = alt.createActions(LocationActions);

LocationActions.js

Alt Stores

var alt = require('/alt');
var LocationActions = require('../actions/LocationActions');

class LocationStore {
  constructor() {
    this.locations = [];

    this.bindListeners({
      handleUpdateLocations: LocationActions.UPDATE_LOCATIONS
    });
  }

  handleUpdateLocations(locations) {
    this.locations = locations;
  }
}

module.exports = alt.createStore(LocationStore, 'LocationStore');
  • locations is the initial data
  • when updateLocation called handle with function
  • handling function updates the data

LocationStore.js

Alt Views (React)

var React = require('react');
var LocationStore = require('../stores/LocationStore');

var Locations = React.createClass({
  getInitialState() {
    return LocationStore.getState();
  },

  componentDidMount() {
    LocationStore.listen(this.onChange);
  },

  componentWillUnmount() {
    LocationStore.unlisten(this.onChange);
  },

  onChange(state) {
    this.setState(state);
  },

  render() {
    return (
      <ul>
        {this.state.locations.map((location) => {
          return (
            <li>{location.name}</li>
          );
        })}
      </ul>
    );
  }
});

module.exports = Locations;

Redux

  • core principales 
    • single state tree
    • state is read only
    • changes are made by pure functions

Redux Actions

export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

// action creators - functions that create actions
// actions are JS objects in the form of {type: ACTION, data}

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Redux Reducers

  • In alt stores modified state from action in store
  • In Redux reducers take actions and return the modified state to the store
  • Reducers are pure functions
    • output is determined by input
    • start with an initial state
import { combineReducers } from 'redux'
import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Redux Store

import { createStore } from 'redux'
import todoApp from './reducers'

let store = createStore(todoApp)

import { addTodo, 
        toggleTodo, 
        setVisibilityFilter, 
        VisibilityFilters } from './actions'

// Log the initial state
console.log(store.getState())

// Every time the state changes, log it
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// Stop listening to state updates
unsubscribe()

Usage with React

  • Container Components
  • Presentation Components

Container Components

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

Presentation Component

l

import React, { PropTypes } from 'react'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map(todo =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => onTodoClick(todo.id)}
      />
    )}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
  }).isRequired).isRequired,
  onTodoClick: PropTypes.func.isRequired
}

export default TodoList
import React, { PropTypes } from 'react'

const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo

Take Aways

  • Flux is an architecture
  • Action, Stores, Views, dispatcher
  • Implementations of Flux
    • Redux
    • Alt
    • +++++
  • Flux helps figure out data flow

Fluxing Around

By Kyle Potts

Fluxing Around

  • 167