From aa423d4b6282a7d1bba976f10eebe0d84475a32a Mon Sep 17 00:00:00 2001 From: Nathan Wenneker Date: Fri, 6 Nov 2015 16:59:56 -0700 Subject: [PATCH 1/9] Use devtools --- index.js | 17 ++++++++++++----- package.json | 3 ++- store/configureStore.js | 8 ++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 9fd15be..cc6858b 100644 --- a/index.js +++ b/index.js @@ -3,14 +3,21 @@ import { render } from 'react-dom' import { createStore } from 'redux' import { Provider } from 'react-redux' import App from './containers/App' -import todoApp from './reducers' +import { DevTools, LogMonitor, DebugPanel } from 'redux-devtools/lib/react'; +import configureStore from './store/configureStore' -let store = createStore(todoApp) + +let store = configureStore() let rootElement = document.getElementById('root') render( - - - , +
+ + + + + + +
, rootElement ) diff --git a/package.json b/package.json index 64d9ed9..476b832 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "react-dom": "^0.14.0", "react-redux": "^4.0.0", "redux": "^3.0.0", - "reselect": "^2.0.0" + "reselect": "^2.0.0", + "redux-devtools": "^2.1.5" }, "devDependencies": { "babel-core": "^5.6.18", diff --git a/store/configureStore.js b/store/configureStore.js index 479c02c..e6d0afb 100644 --- a/store/configureStore.js +++ b/store/configureStore.js @@ -1,8 +1,12 @@ -import { createStore } from 'redux' +import { compose, createStore } from 'redux'; import rootReducer from '../reducers' +import { devTools } from 'redux-devtools'; export default function configureStore(initialState) { - const store = createStore(rootReducer, initialState) + + const finalCreateStore = compose(devTools())(createStore); + + const store = finalCreateStore(rootReducer, initialState) if (module.hot) { // Enable Webpack hot module replacement for reducers From 393c18a42174fc945c3e7521739e625ad9e876bd Mon Sep 17 00:00:00 2001 From: Nathan Wenneker Date: Fri, 6 Nov 2015 17:00:28 -0700 Subject: [PATCH 2/9] introduce panes --- actions.js | 18 ++++++--- containers/App.js | 93 +++++++++++++++++++++++++++++++++-------------- reducers.js | 28 +++++++++++--- 3 files changed, 101 insertions(+), 38 deletions(-) diff --git a/actions.js b/actions.js index 7ee5d03..2e238b3 100644 --- a/actions.js +++ b/actions.js @@ -4,9 +4,10 @@ export const ADD_TODO = 'ADD_TODO' export const COMPLETE_TODO = 'COMPLETE_TODO' -export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' +export const SET_PANE_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' export const CHANGE_THEME = 'CHANGE_THEME' -export const UPDATE_SEARCH = 'UPDATE_SEARCH'; +export const UPDATE_PANE_SEARCH = 'UPDATE_SEARCH'; +export const ADD_PANE = 'ADD_PANE' /* * other constants @@ -22,6 +23,10 @@ export const VisibilityFilters = { * action creators */ +export function addPane() { + return { type: ADD_PANE }; +} + export function addTodo(text) { return { type: ADD_TODO, text } } @@ -30,17 +35,18 @@ export function completeTodo(index) { return { type: COMPLETE_TODO, index } } -export function setVisibilityFilter(filter) { - return { type: SET_VISIBILITY_FILTER, filter } +export function setPaneVisibilityFilter(paneIdx, filter) { + return { type: SET_PANE_VISIBILITY_FILTER, index: paneIdx, filter } } export function changeTheme() { return { type: CHANGE_THEME }; } -export function updateSearch(searchTerm) { +export function updatePaneSearch(paneIdx, searchTerm) { return { - type: UPDATE_SEARCH, + type: UPDATE_PANE_SEARCH, + index: paneIdx, searchTerm }; } diff --git a/containers/App.js b/containers/App.js index ce71cd8..fe572d2 100644 --- a/containers/App.js +++ b/containers/App.js @@ -1,30 +1,22 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { addTodo, completeTodo, setVisibilityFilter, changeTheme, VisibilityFilters, updateSearch } from '../actions' +import { addTodo, completeTodo, setPaneVisibilityFilter, changeTheme, VisibilityFilters, updatePaneSearch, addPane } from '../actions' import AddTodo from '../components/AddTodo' import TodoList from '../components/TodoList' import Footer from '../components/Footer' import { memoize, createMemoizedFunction } from '../memoize' import { createSelector, createStructuredSelector } from 'reselect'; -class App extends Component { - - updateSearch = function(e) { - const { dispatch } = this.props; - dispatch(updateSearch(e.target.value)); - } +class Pane extends Component { render() { - console.log(this.props); - // Injected by connect() call: - const { dispatch, matchingVisibleTodos, currentTheme, searchTerm, visibilityFilter } = this.props + const { dispatch, updateSearch, setVisibilityFilter, searchTerm, visibilityFilter, matchingVisibleTodos } = this.props; + return ( -
- Search:
+
+ Search:
- dispatch(addTodo(text)) - } /> + onAddClick={text => dispatch(addTodo(text)) } /> @@ -35,14 +27,56 @@ class App extends Component { onFilterChange={nextFilter => dispatch(setVisibilityFilter(nextFilter)) } /> +
+ ); + } +} + + +class App extends Component { + + render() { + console.log(this.props); + const { dispatch, panes, currentTheme, matchingVisibleTodos } = this.props + + var createUpdatePaneSearch = function(paneIdx) { + return (e) => { + dispatch(updatePaneSearch(paneIdx, e.target.value)); + }; + } + + var createSetVisibilityFilter = function(paneIdx) { + return (filter) => { + dispatch(setPaneVisibilityFilter(paneIdx, filter)); + } + } + + let paneComponents = panes.map((pane, idx) => + + ); + + // console.log("Pane components are:"); + // console.log(paneComponents); + + return ( +
+ {paneComponents} +
) } } -App.propTypes = { - todos: PropTypes.arrayOf(PropTypes.shape({ +Pane.propTypes = { + matchingVisibleTodos: PropTypes.arrayOf(PropTypes.shape({ text: PropTypes.string.isRequired, completed: PropTypes.bool.isRequired })), @@ -71,25 +105,30 @@ function selectMatchingTodos(todos, search) { } const todosSelector = state => state.todos; -const visibilityFilterSelector = state => state.visibilityFilter; +// const visibilityFilterSelector = state => state.visibilityFilter; const currentThemeSelector = state => state.currentTheme; -const searchTermSelector = state => state.searchTerm; +// const searchTermSelector = state => state.searchTerm; +const panesSelector = state => state.panes; -const visibleTodosSelector = createSelector( - [todosSelector, visibilityFilterSelector], - selectVisibleTodos -); +// const visibleTodosSelector = createSelector( +// [todosSelector, visibilityFilterSelector], +// selectVisibleTodos +// ); + +// const matchingVisibleTodosSelector = createSelector( +// [visibleTodosSelector, searchTermSelector], +// selectMatchingTodos +// ); const matchingVisibleTodosSelector = createSelector( - [visibleTodosSelector, searchTermSelector], - selectMatchingTodos + [state => state.panes, state => state.todos], + (panes, todos) => panes.map( (pane) => selectMatchingTodos(selectVisibleTodos(todos, pane.visibilityFilter), pane.searchTerm) ) ); const select = createStructuredSelector({ + panes: panesSelector, matchingVisibleTodos: matchingVisibleTodosSelector, - visibilityFilter: visibilityFilterSelector, currentTheme: currentThemeSelector, - searchTerm: searchTermSelector }); // Wrap the component to inject dispatch and state into it diff --git a/reducers.js b/reducers.js index bc73343..3618da5 100644 --- a/reducers.js +++ b/reducers.js @@ -1,10 +1,10 @@ import { combineReducers } from 'redux' -import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, CHANGE_THEME, VisibilityFilters, UPDATE_SEARCH } from './actions' +import { ADD_PANE, ADD_TODO, COMPLETE_TODO, SET_PANE_VISIBILITY_FILTER, CHANGE_THEME, VisibilityFilters, UPDATE_PANE_SEARCH } from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { - case SET_VISIBILITY_FILTER: + case SET_PANE_VISIBILITY_FILTER: return action.filter default: return state @@ -45,19 +45,37 @@ function currentTheme(state = 'theme-green', action) { function searchTerm(state = '', action) { switch (action.type) { - case UPDATE_SEARCH: + case UPDATE_PANE_SEARCH: return action.searchTerm; default: return state; } } +const paneReducer = combineReducers({ + visibilityFilter, + searchTerm +}); + +function panes(state = [], action) { + console.log("running panes reducer for action: "); + console.log(action); + switch(action.type) { + case ADD_PANE: + console.log("adding pane"); + return state.concat(paneReducer(undefined, {type: null})); + case SET_PANE_VISIBILITY_FILTER: + case UPDATE_PANE_SEARCH: + return state.map((pane, idx) => idx === action.index ? paneReducer(pane, action) : pane); + default: + return state; + } +} const todoApp = combineReducers({ - visibilityFilter, + panes, todos, currentTheme, - searchTerm }) export default todoApp From dbbd0ce3ab3d10a75cebe460750933693125542b Mon Sep 17 00:00:00 2001 From: Nathan Wenneker Date: Sat, 7 Nov 2015 11:14:54 -0700 Subject: [PATCH 3/9] Fix issue with visibilityFilter --- containers/App.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/containers/App.js b/containers/App.js index fe572d2..e67be7b 100644 --- a/containers/App.js +++ b/containers/App.js @@ -24,9 +24,8 @@ class Pane extends Component { } />
- dispatch(setVisibilityFilter(nextFilter)) - } /> + onFilterChange={setVisibilityFilter} + />
); } From dddeddf70d2d28fd94b31e63d4323d48f6262636 Mon Sep 17 00:00:00 2001 From: Nathan Wenneker Date: Sat, 7 Nov 2015 11:11:38 -0700 Subject: [PATCH 4/9] introduce collection reducers --- combineCollectionReducers.js | 24 ++++++++++++++++++++++ reducers.js | 40 ++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 20 deletions(-) create mode 100644 combineCollectionReducers.js diff --git a/combineCollectionReducers.js b/combineCollectionReducers.js new file mode 100644 index 0000000..73147e5 --- /dev/null +++ b/combineCollectionReducers.js @@ -0,0 +1,24 @@ +// There is a common pattern where the state contains +// a collection of like objects, with some actions +// needing to affect an element of the collection (i.e. complete a given todo element) +// while other actions affect the collection as a whole (i.e. add a new todo element). +// +// Generally actions, that affect a given todo need to pass in an `index` so we know +// which todo the action should affect. +// +// This function takes two arguments: the first is a collectionReducer that knows how +// to handle actions against the collection as a whole. +// The second argument is an elementReducer that knows how to handle +// actions against a specific element. +// +// The goal is to extract the "index-detection" and state reconstruction logic, so +// that you only have to focus on how to reduce collection actions and element actions. + +export default function combineCollectionReducers(collectionReducer, elementReducer) { + return function(state = collectionReducer(undefined, { type: null }), action) { + if(action.index !== undefined) { + return state.map((element, idx) => idx === action.index ? elementReducer(element, action) : element); + } + return collectionReducer(state, action); + } +} diff --git a/reducers.js b/reducers.js index 3618da5..df4f515 100644 --- a/reducers.js +++ b/reducers.js @@ -1,5 +1,7 @@ import { combineReducers } from 'redux' import { ADD_PANE, ADD_TODO, COMPLETE_TODO, SET_PANE_VISIBILITY_FILTER, CHANGE_THEME, VisibilityFilters, UPDATE_PANE_SEARCH } from './actions' +import combineCollectionReducers from './combineCollectionReducers' + const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { @@ -11,8 +13,17 @@ function visibilityFilter(state = SHOW_ALL, action) { } } -function todos(state = [], action) { - switch (action.type) { +function todoElementReducer(state, action) { + switch(action.type) { + case COMPLETE_TODO: + return Object.assign({}, state, { completed: true }); + default: + return state; + } +} + +function todoCollectionReducer(state = [], action) { + switch(action.type) { case ADD_TODO: return [ ...state, @@ -21,16 +32,8 @@ function todos(state = [], action) { completed: false } ] - case COMPLETE_TODO: - return [ - ...state.slice(0, action.index), - Object.assign({}, state[action.index], { - completed: true - }), - ...state.slice(action.index + 1) - ] default: - return state + return state; } } @@ -52,26 +55,23 @@ function searchTerm(state = '', action) { } } -const paneReducer = combineReducers({ +const paneElementReducer = combineReducers({ visibilityFilter, searchTerm }); -function panes(state = [], action) { - console.log("running panes reducer for action: "); - console.log(action); +function paneCollectionReducer(state = [], action) { switch(action.type) { case ADD_PANE: - console.log("adding pane"); - return state.concat(paneReducer(undefined, {type: null})); - case SET_PANE_VISIBILITY_FILTER: - case UPDATE_PANE_SEARCH: - return state.map((pane, idx) => idx === action.index ? paneReducer(pane, action) : pane); + return state.concat(paneElementReducer(undefined, {type: null})); default: return state; } } +const panes = combineCollectionReducers(paneCollectionReducer, paneElementReducer); +const todos = combineCollectionReducers(todoCollectionReducer, todoElementReducer); + const todoApp = combineReducers({ panes, todos, From 6cbc0c0657dd84f13c32f0fd171496100a0a0173 Mon Sep 17 00:00:00 2001 From: jgautsch Date: Sat, 7 Nov 2015 13:12:08 -0600 Subject: [PATCH 5/9] Clean up/organize files a bit --- components/Pane.js | 41 ++++++++++ containers/App.js | 45 +---------- reducers.js | 81 ------------------- reducers/CurrentThemeReducer.js | 10 +++ reducers/PanesReducer.js | 39 +++++++++ reducers/TodosReducer.js | 28 +++++++ .../combineCollectionReducers.js | 2 +- reducers/index.js | 10 +++ 8 files changed, 132 insertions(+), 124 deletions(-) create mode 100644 components/Pane.js delete mode 100644 reducers.js create mode 100644 reducers/CurrentThemeReducer.js create mode 100644 reducers/PanesReducer.js create mode 100644 reducers/TodosReducer.js rename combineCollectionReducers.js => reducers/combineCollectionReducers.js (96%) create mode 100644 reducers/index.js diff --git a/components/Pane.js b/components/Pane.js new file mode 100644 index 0000000..87d35b1 --- /dev/null +++ b/components/Pane.js @@ -0,0 +1,41 @@ +import React, { PropTypes, Component } from 'react'; +import { addTodo, completeTodo } from '../actions' +import AddTodo from './AddTodo'; +import TodoList from './TodoList'; +import Footer from './Footer'; + +class Pane extends Component { + render() { + const { dispatch, updateSearch, setVisibilityFilter, searchTerm, visibilityFilter, matchingVisibleTodos } = this.props; + + return ( +
+ Search: +
+ dispatch(addTodo(text)) } /> + dispatch(completeTodo(index)) } + /> +
+
+ ); + } +} + +Pane.propTypes = { + matchingVisibleTodos: PropTypes.arrayOf(PropTypes.shape({ + text: PropTypes.string.isRequired, + completed: PropTypes.bool.isRequired + })), + visibilityFilter: PropTypes.oneOf([ + 'SHOW_ALL', + 'SHOW_COMPLETED', + 'SHOW_ACTIVE' + ]).isRequired +}; + +export default Pane; diff --git a/containers/App.js b/containers/App.js index e67be7b..d4f83a5 100644 --- a/containers/App.js +++ b/containers/App.js @@ -1,35 +1,9 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import { addTodo, completeTodo, setPaneVisibilityFilter, changeTheme, VisibilityFilters, updatePaneSearch, addPane } from '../actions' -import AddTodo from '../components/AddTodo' -import TodoList from '../components/TodoList' -import Footer from '../components/Footer' +import { setPaneVisibilityFilter, changeTheme, VisibilityFilters, updatePaneSearch, addPane } from '../actions' import { memoize, createMemoizedFunction } from '../memoize' import { createSelector, createStructuredSelector } from 'reselect'; - -class Pane extends Component { - - render() { - const { dispatch, updateSearch, setVisibilityFilter, searchTerm, visibilityFilter, matchingVisibleTodos } = this.props; - - return ( -
- Search:
- dispatch(addTodo(text)) } /> - - dispatch(completeTodo(index)) - } /> -
-
- ); - } -} +import Pane from '../components/Pane'; class App extends Component { @@ -61,9 +35,6 @@ class App extends Component { /> ); - // console.log("Pane components are:"); - // console.log(paneComponents); - return (
{paneComponents} @@ -74,17 +45,7 @@ class App extends Component { } } -Pane.propTypes = { - matchingVisibleTodos: PropTypes.arrayOf(PropTypes.shape({ - text: PropTypes.string.isRequired, - completed: PropTypes.bool.isRequired - })), - visibilityFilter: PropTypes.oneOf([ - 'SHOW_ALL', - 'SHOW_COMPLETED', - 'SHOW_ACTIVE' - ]).isRequired -} + function selectVisibleTodos(todos, filter) { console.log("Recalculating selectTodos"); diff --git a/reducers.js b/reducers.js deleted file mode 100644 index df4f515..0000000 --- a/reducers.js +++ /dev/null @@ -1,81 +0,0 @@ -import { combineReducers } from 'redux' -import { ADD_PANE, ADD_TODO, COMPLETE_TODO, SET_PANE_VISIBILITY_FILTER, CHANGE_THEME, VisibilityFilters, UPDATE_PANE_SEARCH } from './actions' -import combineCollectionReducers from './combineCollectionReducers' - -const { SHOW_ALL } = VisibilityFilters - -function visibilityFilter(state = SHOW_ALL, action) { - switch (action.type) { - case SET_PANE_VISIBILITY_FILTER: - return action.filter - default: - return state - } -} - -function todoElementReducer(state, action) { - switch(action.type) { - case COMPLETE_TODO: - return Object.assign({}, state, { completed: true }); - default: - return state; - } -} - -function todoCollectionReducer(state = [], action) { - switch(action.type) { - case ADD_TODO: - return [ - ...state, - { - text: action.text, - completed: false - } - ] - default: - return state; - } -} - -function currentTheme(state = 'theme-green', action) { - switch (action.type) { - case CHANGE_THEME: - return state == 'theme-green' ? 'theme-blue' : 'theme-green'; - default: - return state - } -} - -function searchTerm(state = '', action) { - switch (action.type) { - case UPDATE_PANE_SEARCH: - return action.searchTerm; - default: - return state; - } -} - -const paneElementReducer = combineReducers({ - visibilityFilter, - searchTerm -}); - -function paneCollectionReducer(state = [], action) { - switch(action.type) { - case ADD_PANE: - return state.concat(paneElementReducer(undefined, {type: null})); - default: - return state; - } -} - -const panes = combineCollectionReducers(paneCollectionReducer, paneElementReducer); -const todos = combineCollectionReducers(todoCollectionReducer, todoElementReducer); - -const todoApp = combineReducers({ - panes, - todos, - currentTheme, -}) - -export default todoApp diff --git a/reducers/CurrentThemeReducer.js b/reducers/CurrentThemeReducer.js new file mode 100644 index 0000000..66f3012 --- /dev/null +++ b/reducers/CurrentThemeReducer.js @@ -0,0 +1,10 @@ +import { CHANGE_THEME } from '../actions' + +export function currentTheme(state = 'theme-green', action) { + switch (action.type) { + case CHANGE_THEME: + return state == 'theme-green' ? 'theme-blue' : 'theme-green'; + default: + return state + } +} diff --git a/reducers/PanesReducer.js b/reducers/PanesReducer.js new file mode 100644 index 0000000..270409e --- /dev/null +++ b/reducers/PanesReducer.js @@ -0,0 +1,39 @@ +import { combineReducers } from 'redux' +import { ADD_PANE, SET_PANE_VISIBILITY_FILTER, VisibilityFilters, UPDATE_PANE_SEARCH } from '../actions' +import combineCollectionReducers from './combineCollectionReducers' + +const { SHOW_ALL } = VisibilityFilters + +function searchTerm(state = '', action) { + switch (action.type) { + case UPDATE_PANE_SEARCH: + return action.searchTerm; + default: + return state; + } +} + +function visibilityFilter(state = SHOW_ALL, action) { + switch (action.type) { + case SET_PANE_VISIBILITY_FILTER: + return action.filter + default: + return state + } +} + +const paneElementReducer = combineReducers({ + visibilityFilter, + searchTerm +}); + +function paneCollectionReducer(state = [], action) { + switch(action.type) { + case ADD_PANE: + return state.concat(paneElementReducer(undefined, {type: null})); + default: + return state; + } +} + +export const panes = combineCollectionReducers(paneCollectionReducer, paneElementReducer); diff --git a/reducers/TodosReducer.js b/reducers/TodosReducer.js new file mode 100644 index 0000000..2bf0b88 --- /dev/null +++ b/reducers/TodosReducer.js @@ -0,0 +1,28 @@ +import { ADD_TODO, COMPLETE_TODO } from '../actions' +import combineCollectionReducers from './combineCollectionReducers' + +function todoElementReducer(state, action) { + switch(action.type) { + case COMPLETE_TODO: + return Object.assign({}, state, { completed: true }); + default: + return state; + } +} + +function todoCollectionReducer(state = [], action) { + switch(action.type) { + case ADD_TODO: + return [ + ...state, + { + text: action.text, + completed: false + } + ] + default: + return state; + } +} + +export const todos = combineCollectionReducers(todoCollectionReducer, todoElementReducer); diff --git a/combineCollectionReducers.js b/reducers/combineCollectionReducers.js similarity index 96% rename from combineCollectionReducers.js rename to reducers/combineCollectionReducers.js index 73147e5..a36327b 100644 --- a/combineCollectionReducers.js +++ b/reducers/combineCollectionReducers.js @@ -9,7 +9,7 @@ // This function takes two arguments: the first is a collectionReducer that knows how // to handle actions against the collection as a whole. // The second argument is an elementReducer that knows how to handle -// actions against a specific element. +// actions against a specific element. // // The goal is to extract the "index-detection" and state reconstruction logic, so // that you only have to focus on how to reduce collection actions and element actions. diff --git a/reducers/index.js b/reducers/index.js new file mode 100644 index 0000000..bc3d23f --- /dev/null +++ b/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from 'redux' +import { currentTheme } from './CurrentThemeReducer'; +import { todos } from './TodosReducer'; +import { panes } from './PanesReducer'; + +export default combineReducers({ + panes, + todos, + currentTheme, +}); From db5abb237537ee90fb74df87dc9493d1f1b283a6 Mon Sep 17 00:00:00 2001 From: jgautsch Date: Mon, 9 Nov 2015 15:05:28 -0600 Subject: [PATCH 6/9] Use hybrid selector memoizing factory function approach --- components/Pane.js | 15 +++++++++-- containers/App.js | 58 +++++----------------------------------- package.json | 4 ++- reducers/PanesReducer.js | 2 ++ selectors/AppSelector.js | 42 +++++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 55 deletions(-) create mode 100644 selectors/AppSelector.js diff --git a/components/Pane.js b/components/Pane.js index 87d35b1..d10e7a6 100644 --- a/components/Pane.js +++ b/components/Pane.js @@ -6,7 +6,18 @@ import Footer from './Footer'; class Pane extends Component { render() { - const { dispatch, updateSearch, setVisibilityFilter, searchTerm, visibilityFilter, matchingVisibleTodos } = this.props; + const { + dispatch, + pane, + matchingVisibleTodosForPaneFactory, + updateSearch, + setVisibilityFilter, + ...props + } = this.props; + + const { visibilityFilter, searchTerm } = pane; + + const visibleTodos = matchingVisibleTodosForPaneFactory(pane); return (
@@ -14,7 +25,7 @@ class Pane extends Component {
dispatch(addTodo(text)) } /> dispatch(completeTodo(index)) } />