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