Redux is FLUX inspired architecture which is becoming de-facto standard for building React applications. But way? The talk tries to explain that.
(Video: https://www.youtube.com/watch?v=zdY1YqjjT6s)
React & ReduxRadoslav Stankov 27/03/2016
View Slide
Radoslav Stankov@rstankovhttp://rstankov.comhttp://github.com/rstankov
https://github.com/rstankov/talks-code
React
const FILTERS = {All: () => true,Active: (t) => !t.completed,Completed: (t) => t.completed,};export default class App extends React.Component {constructor(props) {super(props);this.state = {filter: 'All',todos: [],};}render() {return ({this.filteredTodos().map((todo) => (
render() {return ({this.filteredTodos().map((todo) => ({todo.text}))}{this.counterText()}{Object.keys(FILTERS).map((filterName) => ())});}
counterText() {const count = this.state.todos.filter((t) => !t.completed).length;return `${ count } ${ count === 1 ? 'item' : 'items' } left`;}filteredTodos() {return this.state.todos.filter(FILTERS[this.state.filter]);}showCompletedCount() {return this.todos.filter((t) => t.completed).length > 0;}handleNewTodo(text) {const todo = {id: +(new Date()),text: text,completed: false};this.setState({todos: [todo].concat(this.state.todos),});}
filteredTodos() {return this.state.todos.filter(FILTERS[this.state.filter]);}showCompletedCount() {return this.todos.filter((t) => t.completed).length > 0;}handleNewTodo(text) {const todo = {id: +(new Date()),text: text,completed: false};this.setState({todos: [todo].concat(this.state.todos),});}handleClearCompleted() {this.setState({todos: this.state.todos.filter((t) => !t.completed),});}
this.setState({todos: [todo].concat(this.state.todos),});}handleClearCompleted() {this.setState({todos: this.state.todos.filter((t) => !t.completed),});}removeTodoHandler(todo) {return (e) => {this.setState({todos: this.state.todos.filter((t) => t.id !== todo.id),});};}toggleTodoHandler(todo) {return (e) => {this.setState({todos: this.state.todos.map((t) => {if (t.id === todo.id) {
});};}toggleTodoHandler(todo) {return (e) => {this.setState({todos: this.state.todos.map((t) => {if (t.id === todo.id) {t.completed = !t.completed;}return t;}),});};}filterHander(filterName) {return (e) => {this.setState({filter: filterName,});};}}
Where is all the data?
[todos] [filter]
export default class App extends React.Component {// . . .render() {return (addTodo={this.addTodo} />todos={this.state.todos.filter(FILTERS[this.state.filter])}toggleTodo={this.updateTodo}removeTodo={this.removeTodo} />todos={this.state.todos}filters={FILTERS}activeFilter={this.state.filter}changeFilter={this.changeFilter}clearCompletedTodos={this.clearCompletedTodos} />);}}
export default class TodoList extends React.Component {static propTypes = {toggleTodo: React.PropTypes.func.isRequired,removeTodo: React.PropTypes.func.isRequired,todos: React.PropTypes.array.isRequired,};render() {return ({this.props.todos.map((todo) => (key={todo.id}todo={todo}toggleTodo={this.props.updateTodo}removeTodo={this.props.removeTodo} />))});}}
Let’s add new action
export default class App extends React.Component {// . . .render() {return (addTodo={this.addTodo} />todos={this.state.todos.filter(FILTERS[this.state.filter])}toggleTodo={this.updateTodo} updateTodo={this.updateTodo}removeTodo={this.removeTodo} />todos={this.state.todos}filters={FILTERS}activeFilter={this.state.filter}changeFilter={this.changeFilter}clearCompletedTodos={this.clearCompletedTodos} />);}}
export default class TodoList extends React.Component {static propTypes = {toggleTodo: React.PropTypes.func.isRequired,removeTodo: React.PropTypes.func.isRequired,updateTodo: React.PropTypes.func.isRequired,todos: React.PropTypes.array.isRequired,};render() {return ({this.props.todos.map((todo) => (key={todo.id}todo={todo}toggleTodo={this.props.updateTodo}toggleTodo={this.props.updateTodo}removeTodo={this.props.removeTodo} />))});}}
DispatcherActionViewStore
DispatcherAction APIViewStore
class TodosStore extends ReduceStore {getInitialState() {return [];}reduce (state, action) {switch (action.type) {case 'todo/add':return [action.todo].concat(state);case 'todo/toggle':return state.map((t) => t.id === action.id ? { ...t, completed:case 'todo/remove':return state.filter((t) => t.id !== action.id);case 'todo/clear':return state.filter((t) => !t.completed)default:return state;}}}
const dispatcher = new Dispatcher();const store = new TodosStore(dispatcher);store.getState() // => []
const dispatcher = new Dispatcher();const store = new TodosStore(dispatcher);store.getState() // => [] dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' });store.getState() // => [{ id: 1, text: 'Task 1', completed: false }]
const dispatcher = new Dispatcher();const store = new TodosStore(dispatcher);store.getState() // => [] dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' });store.getState() // => [{ id: 1, text: 'Task 1', completed: false }]dispatcher.dispatch({ type: 'todo/toggle', id: 1 });store.getState() // => [{ id: 1, text: 'Task 1', completed: true }]
const dispatcher = new Dispatcher();const store = new TodosStore(dispatcher);store.getState() // => [] dispatcher.dispatch({ type: 'todo/add' text: 'Task 1' });store.getState() // => [{ id: 1, text: 'Task 1', completed: false }]dispatcher.dispatch({ type: 'todo/toggle', id: 1 });store.getState() // => [{ id: 1, text: 'Task 1', completed: true }]dispatcher.dispatch({ type: 'todo/remove', id: 1 });store.getState() // => []
class FilterStore extends ReduceStore {getInitialState() {return { name: 'All', filter: Filters['All'] };}reduce (state, action) {switch (action.type) {case 'filter/select':const name = action.filterName;const filter = FILTERS[name];if (!filter) {return state;}return { name: name, filter: filter };default:return state;}}}
import {Container} from 'flux/utils';class TodoList extends React.Component {static getStores() {return [TodosStore, FiltersStore];}static calculateState() {return {todos: TodosStore.getState(),filter: FiltersStore.getState().filter,};}render() {const todos = this.state.todos.filter(this.state.filter);return ({todos.map((todo) => )});}}export default Container.create(TodoList);
export default class TodoItem extends React.Component {static propTypes = {todo: React.PropTypes.object.isRequired,};handleRemove() {removeTodo(this.props.todo);}handleToggle() {toggleTodo(this.props.todo);}render() {const todo = this.props;return ({todo.text});}}
import {dispatch} from 'stores/dispatcher'; export function toggleTodo(todo) {dispatch({type: 'todo/toggle',id: todo.id,});}export function removeTodo(todo) {dispatch({type: 'todo/remove',id: todo.id,});}export function clearCompletedTodos() {dispatch({type: 'todo/clear',});}
const dispatcher = new Dispatcher();const store = new TodosStore(dispatcher);
• Global state• Communication between multiple stores• Dispatch locks• TestingIssues with FLUX
Redux
DispatcherActionViewStore Store
ActionViewStoreReducer(s)Reducer(s)
let store = {todos: [],filter: 'All',};
let store = {todos: [],filter: 'All',};store = reduce(store, {action: 'todo/add', todo: todo});store.todos // # => [ todo ]
let store = {todos: [],filter: 'All',};store = reduce(store, {action: 'todo/add', todo: todo});store.todos // # => [ todo ]store = reduce(store, {action: 'todo/toggle', id: todo.id});store = reduce(store, {action: 'todo/clear', id: todo.id});store.todos // # => [ ]
let store = {todos: [],filter: 'All',};store = reduce(store, {action: 'todo/add', todo: todo});store.todos // # => [ todo ]store = reduce(store, {action: 'todo/toggle', id: todo.id});store = reduce(store, {action: 'todo/clear', id: todo.id});store.todos // # => [ ]store = reduce(store, {action: 'filter/select', name: 'Active'});store.filter // # => 'Active'
StoreState: todo todoReducerState: filter filterReducer
// reducers/index.js import {combineReducers} from 'redux'import todos from './todos'import visibilityFilter from './visibilityFilter'export default combineReducers({todos: todos,visibilityFilter: visibilityFilter,});
// reducers/todos.jsexport default function(state = [], action) {switch (action.type) {case 'todo/add':return [action.todo].concat(state);case 'todo/toggle':return state.map((t) => t.id === action.id ? { ...t, completed: tcase 'todo/remove':return state.filter((t) => t.id !== action.id);case 'todo/clear':return state.filter((t) => !t.completed)default:return state;}}
// reducers/visibilityFilter.jsexport default function(state, action) {if (!state) {state = DEFAULT_STATE;}switch (action.type) {case 'filter/select':const name = action.filterName;const filter = FILTERS[name];if (!filter) {return state;}return { name: name, filter: filter };default:return state;}}
import React from 'react'import {render} from 'react-dom'import {Provider} from 'react-redux'import {createStore} from 'redux'import AppReducer from './reducers'import App from './components/App'let store = createStore(AppReducer)let root = ();render(root, document.getElementById('root'));
Store[todos] [filter]
export default class App extends React.Component {render() {return ();}}
import {connect} from 'react-redux'class TodoList extends React.Component {render() {const todos = this.props.todos.filter(this.props.filter);return ({todos.map((todo) => )});}}const decorate = connect((state) => {return {todos: state.todos,filter: state.visibilityFilter.filter,};});export default decorate(TodoList);
Let's write some tests
import {connect} from 'react-redux'export class TodoList extends React.Component {render() {const todos = this.props.todos.filter(this.props.filter);return ({todos.map((todo) => )});}}const decorate = connect((state) => {return {todos: state.todos,filter: state.visibilityFilter.filter,};});export default decorate(TodoList);
// Using: chai, enzyme, chai-enzymeimport {TodoList} from 'components/TodoList';import {TodoItem} from 'components/TodoItem';describe(TodoList.dispalyName, () => {const all = () => trueconst none = () => falseit("renders list of TodoItem(s)", () => {const todos = [fakeTodo()]; const element = shallow(); expect(element).to.have.descendants();});it("filters with filter", () => {const todos = [fakeTodo()];const element = shallow();expect(element).to.have.descendants();});});
export class TodoItem extends React.Component {static propTypes = {todo: React.PropTypes.object.isRequired,dispatch: React.PropTypes.func.isRequired,};handleRemove() {this.props.dispatch(removeTodo(this.props.todo));}handleToggle() {this.props.dispatch(toggleTodo(this.props.todo));}render() {return ({this.props.todo.text});}}export default connect()(TodoItem);
export function toggleTodo(todo) {return {type: 'todo/toggle',id: todo.id,};}export function removeTodo(todo) {return {type: 'todo/remove',id: todo.id,};}export function clearCompletedTodos() {return {type: 'todo/clear',};}
Redux friends• selectors / https://github.com/reactjs/reselect /• async / https://github.com/gaearon/redux-thunk /• router / https://github.com/reactjs/react-router-redux /• data fetching / https://github.com/relax/relate /
https://speakerdeck.com/rstankov/react-and-redux
@rstankovThanks :)
Questions?