Last active
August 30, 2016 05:17
-
-
Save tongrhj/c92bae8914d21e2630164577b49d3a1d to your computer and use it in GitHub Desktop.
Getting Started With Redux
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Practice Exercise to Accompany Egghead Course by Dan Abramov Getting Started with Redux | |
const alphabets = Array.from('abcdefghijklmnopqrstuvwxyz') | |
const abcSnake = (state = ['a'], action) => { | |
deepFreeze(state) | |
switch (action.type) { | |
case 'APPEND': | |
return appendToSnake(state) | |
case 'REMOVE': | |
return removeFromSnake(state); | |
case 'MODIFY': | |
return modifySnake(state, action.newValue) | |
default: | |
return state; | |
} | |
} | |
const appendToSnake = (snake) => { | |
deepFreeze(snake) | |
newABCIndex = alphabets.findIndex((elm) => { | |
return elm == snake.slice(-1) | |
}) + 1 | |
while (newABCIndex >= 26) { newABCIndex -= 26 } | |
// return snake.concat(alphabets[newABCIndex]) | |
return [...snake, alphabets[newABCIndex]].join('') | |
} | |
const removeFromSnake = (snake) => { | |
deepFreeze(snake) | |
return snake.slice(0, -1) | |
} | |
const modifySnake = (snake, newValue) => { | |
return [...snake, ...newValue] | |
} | |
const AbcSnake = ({ | |
value, | |
onAppend, | |
onRemove, | |
onModify | |
}) => | |
( | |
<div> | |
<h1>{value}</h1> | |
<button onClick={onAppend}>+</button> | |
<button onClick={onRemove}>-</button> | |
<input onBlur={onModify} type="text" id='snake-input'></input> | |
</div> | |
) | |
const { createStore } = Redux; | |
const store = createStore(abcSnake); | |
const render = () => { | |
ReactDOM.render( | |
<AbcSnake | |
value = {store.getState()} | |
onAppend = {() => { | |
store.dispatch({ type: 'APPEND' }) | |
}} | |
onRemove = {() => { | |
store.dispatch({ type: 'REMOVE' }) | |
}} | |
onModify = {() => { | |
store.dispatch({ type: 'MODIFY', | |
newValue: document.getElementById('snake-input').value | |
}) | |
}} | |
/>, | |
document.getElementById('app') | |
) | |
} | |
store.subscribe(render) | |
render() | |
// TESTS | |
expect(abcSnake('a', { type: 'APPEND' })).toEqual('ab') | |
expect(abcSnake('abcdefghijklmnopqrstuvwxyz', { type: 'APPEND' })).toEqual('abcdefghijklmnopqrstuvwxyza') | |
expect(abcSnake('ab', { type: 'REMOVE' })).toEqual('a') | |
expect(abcSnake('a', { type: 'REMOVE' })).toEqual('') | |
expect(abcSnake('', { type: 'APPEND' })).toEqual('a') | |
expect(abcSnake('', { type: 'REMOVE' })).toEqual('') | |
expect(abcSnake(undefined, {})).toEqual('a') | |
console.log('Tests passed!') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const processTodo = (state = {}, action) => { | |
switch (action.type) { | |
case 'ADD_TODO': | |
return { | |
id: action.id, | |
text: action.text, | |
completed: false | |
} | |
case 'TOGGLE_TODO': | |
if (state.id !== action.id) { | |
return state | |
} | |
return { ...state, completed: !state.completed } | |
default: | |
return state | |
} | |
} | |
const todos = (state = [], action) => { | |
switch (action.type) { | |
case 'ADD_TODO': | |
return [ | |
...state, | |
processTodo(undefined, action) | |
] | |
case 'TOGGLE_TODO': | |
return state.map(t => { | |
return processTodo(t, action) | |
}) | |
default: | |
return state | |
} | |
} | |
const visibilityFilter = (state = 'SHOW_ALL', action) => { | |
switch (action.type) { | |
case 'SET_VISIBILITY_FILTER': | |
return action.filter | |
default: | |
return state | |
} | |
} | |
const getVisibleTodos = (todos, filter) => { | |
switch (filter) { | |
case 'SHOW_ALL': | |
return todos | |
case 'SHOW_ACTIVE': | |
return todos.filter((todo) => { | |
return !todo.completed | |
}) | |
case 'SHOW_COMPLETED': | |
return todos.filter((todo) => { | |
return todo.completed | |
}) | |
default: | |
return todos | |
} | |
} | |
// What Combine Reducer replaces | |
// const todoApp = (state = {}, action) => { | |
// return { | |
// todos: todos(state.todos, action), | |
// visibilityFilter: visibilityFilter(state.visibilityFilter, action) | |
// } | |
// } | |
// What Combine Reducer does | |
// const combineReducers = (reducers) => { | |
// return (state = {}, action) => { | |
// return Object.keys(reducers).reduce( | |
// (nextState, key) => { | |
// nextState[key] = reducers[key](state[key], action) | |
// return nextState | |
// }, {} | |
// ) | |
// } | |
// } | |
const { combineReducers } = Redux; | |
const todoApp = combineReducers({ | |
// todos: todos, | |
// visibilityFilter: visibilityFilter | |
// ES6 Object literal shorthand means the above is | |
todos, | |
visibilityFilter | |
}) | |
const { Component } = React | |
const TodoListItem = ( | |
{handleClick, completed, text} | |
) => { | |
return ( | |
<li onClick={ handleClick } | |
style={{ | |
textDecoration: completed ? | |
'line-through' : | |
'none' | |
}} | |
> | |
{ text } | |
</li> | |
) | |
} | |
const TodoList = ({ todos, handleTodoClick }) => | |
( | |
<ul> | |
{todos.map(todo => | |
<TodoListItem | |
key={todo.id} | |
{...todo} | |
handleClick={() => handleTodoClick(todo.id)} | |
/> | |
)} | |
</ul> | |
) | |
const mapStateToListProps = (state) => { | |
return { | |
todos: getVisibleTodos( | |
state.todos, | |
state.visibilityFilter | |
) | |
} | |
} | |
const mapDispatchToListProps = (dispatch) => { | |
return { | |
handleTodoClick: (id) => { | |
dispatch(toggleTodo(id)) | |
} | |
} | |
} | |
const toggleTodo = (id) => { | |
return { | |
type: 'TOGGLE_TODO', | |
id | |
} | |
} | |
const { connect } = ReactRedux | |
const VisibleTodoList = connect(mapStateToListProps, mapDispatchToListProps)(TodoList) | |
// What connect ()() creates: (Also see Addtodo and FilterLink) | |
// class VisibleTodoList extends Component { | |
// componentDidMount() { | |
// const { store } = this.context | |
// this.unsubscribe = store.subscribe(() => { | |
// this.forceUpdate() | |
// }) | |
// } | |
// componentWillUnmount() { | |
// this.unsubscribe() | |
// } | |
// render () { | |
// const props = this.props | |
// const { store } = this.context | |
// const state = store.getState() | |
// return ( | |
// <TodoList | |
// todos={ getVisibleTodos(state.todos, state.visibilityFilter) } | |
// /> | |
// ) | |
// } | |
// } | |
// VisibleTodoList.contextTypes = { | |
// store: React.PropTypes.object | |
// } | |
let nextTodoId = 0; | |
const addTodo = (text) => { | |
return { | |
type: 'ADD_TODO', | |
id: nextTodoId++, | |
text | |
} | |
} | |
const AddTodoWIP = ({ dispatch }) => { | |
let input; | |
return ( | |
<div> | |
<input ref= { node => { | |
input = node | |
}} /> | |
<button onClick={() => { | |
dispatch(addTodo(input.value)) | |
input.value = '' | |
}}> | |
Add Todo | |
</button> | |
</div> | |
) | |
} | |
// AddTodo.contextTypes = { | |
// store: React.PropTypes.object | |
// } | |
let AddTodo = connect()(AddTodoWIP) | |
const Link = ({ | |
active, | |
children, | |
handleClick | |
}) => { | |
if (active) { | |
return <span>{children}</span>; | |
} | |
return ( | |
<a href='#' | |
onClick={e => { | |
e.preventDefault(); | |
handleClick(); | |
}} | |
> | |
{children} | |
</a> | |
) | |
} | |
// Extracting Container Components | |
const mapStatetoLinkProps = (state, ownProps) => { | |
return { | |
active: ownProps.filter === state.visibilityFilter | |
} | |
} | |
const mapDispatchToLinkProps = (dispatch, ownProps) => { | |
return { | |
handleClick: () => { | |
dispatch(setVisibilityFilter(ownProps.filter)) | |
} | |
} | |
} | |
const setVisibilityFilter = (filter) => { | |
return { | |
type: 'SET_VISIBILITY_FILTER', | |
filter | |
} | |
} | |
const FilterLink = connect( | |
mapStatetoLinkProps, | |
mapDispatchToLinkProps)(Link) | |
// What connect()() creates: | |
// class FilterLink extends Component { | |
// componentDidMount() { | |
// const { store } = this.context | |
// this.unsubscribe = store.subscribe(() => { | |
// this.forceUpdate() | |
// }) | |
// } | |
// componentWillUnmount() { | |
// this.unsubscribe() | |
// } | |
// render () { | |
// const props = this.props | |
// const { store } = this.context | |
// const state = store.getState() | |
// return ( | |
// <Link | |
// active={ props.filter == state.visibilityFilter } | |
// handleClick={ (filter) => { | |
// store.dispatch({ | |
// type: 'SET_VISIBILITY_FILTER', | |
// filter: props.filter | |
// }) | |
// }} | |
// > | |
// { props.children } | |
// </Link> | |
// ) | |
// } | |
// } | |
// FilterLink.contextTypes = { | |
// store: React.PropTypes.object | |
// } | |
const FilterLinks = () => { | |
return ( | |
<p> | |
SHOW: | |
{' '} | |
<FilterLink filter='SHOW_ALL'>ALL</FilterLink> | |
{', '} | |
<FilterLink filter='SHOW_ACTIVE'>ACTIVE</FilterLink> | |
{', '} | |
<FilterLink filter='SHOW_COMPLETED'>COMPLETED</FilterLink> | |
{' '} | |
</p> | |
) | |
} | |
const TodoApp = () => ( | |
<div> | |
<AddTodo /> | |
<VisibleTodoList /> | |
<FilterLinks /> | |
</div> | |
) | |
const { createStore } = Redux | |
const { Provider } = ReactRedux | |
// We want to avoid doing this so | |
// 1) its easier to test our methods by passing mock stores | |
// 2) we can use different stores on our backend for each request | |
// const store = createStore(todoApp); | |
ReactDOM.render( | |
<Provider store={createStore(todoApp)}> | |
<TodoApp /> | |
</Provider>, | |
document.getElementById('app') | |
) | |
const testAddTodo = () => { | |
const stateBefore = []; | |
const action = { | |
id: 0, | |
type: 'ADD_TODO', | |
text: 'Learn Redux', | |
completed: false | |
} | |
const stateAfter =[{ | |
id: 0, | |
text: 'Learn Redux', | |
completed: false | |
}] | |
deepFreeze(stateBefore) | |
deepFreeze(action) | |
expect( | |
todos(stateBefore, action) | |
).toEqual(stateAfter) | |
} | |
const testToggleTodo = () => { | |
const stateBefore = [{ | |
id: 0, | |
text: 'Learn Redux', | |
completed: false, | |
},{ | |
id: 1, | |
text: 'Go pokemon hunting', | |
completed: false, | |
}] | |
const action = { | |
type: 'TOGGLE_TODO', | |
id: 1 | |
} | |
deepFreeze(stateBefore) | |
deepFreeze(action) | |
const stateAfter = [stateBefore[0], | |
Object.assign({}, | |
stateBefore[1], | |
{ completed: true } | |
)] | |
expect( | |
todos(stateBefore, action) | |
).toEqual(stateAfter) | |
} | |
const testVisibilityFilter = () => { | |
const mock_todo = [{ | |
id: 0, | |
text: 'Learn Redux', | |
completed: false, | |
},{ | |
id: 1, | |
text: 'Go pokemon hunting', | |
completed: false, | |
}] | |
const action = { | |
type: 'SET_VISIBILITY_FILTER', | |
filter: 'SHOW_COMPLETED' | |
} | |
const stateBefore = { | |
todos: mock_todo, | |
visibilityFilter: 'SHOW_ALL' | |
} | |
deepFreeze(mock_todo) | |
deepFreeze(stateBefore) | |
deepFreeze(action) | |
const stateAfter = { | |
todos: mock_todo, | |
visibilityFilter: 'SHOW_COMPLETED' | |
} | |
expect( | |
todoApp(stateBefore, action) | |
).toEqual(stateAfter) | |
} | |
testAddTodo() | |
testToggleTodo() | |
testVisibilityFilter() | |
console.log('All tests passed.') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment