diff --git a/index.html b/index.html index c0ed6a3..9d4da2b 100644 --- a/index.html +++ b/index.html @@ -132,7 +132,7 @@
  • - + Redux: Service Calls
  • diff --git a/package-lock.json b/package-lock.json index f73efe1..5e52d08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8527,11 +8527,6 @@ "json-stringify-safe": "^5.0.1" } }, - "redux-react-hook": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/redux-react-hook/-/redux-react-hook-3.2.0.tgz", - "integrity": "sha512-GibqTO/Cgl2nRuhw2wacyfd2Nds8pYAPU/eYuTqXZ9v01CT7s7LSwDfPdtQua7TBqE8XNjrwKZqfDyEtfXt7vQ==" - }, "redux-starter-kit": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/redux-starter-kit/-/redux-starter-kit-0.4.3.tgz", diff --git a/step2-05/exercise/README.md b/step2-05/exercise/README.md index 630fa61..52c9eb2 100644 --- a/step2-05/exercise/README.md +++ b/step2-05/exercise/README.md @@ -2,7 +2,7 @@ [Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/) -If you still have the app running from a previous step, stop it with `ctrl+c`. Start the tests instead by running `npm test` from the root of the `frontend-bootcamp` folder. +If you don't already have the app running, start it by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 5 to see results. 1. First, take a look at the store interface in `exercise/src/store/index.ts`. Note that the `Store` interface has two keys: `todos` and `filter`. We'll concentrate on `todos`, which is an object where the keys are string IDs and the values are of a `TodoItem` type. diff --git a/step2-06/demo/src/components/TodoListItem.tsx b/step2-06/demo/src/components/TodoListItem.tsx index cd6cc74..2ac0ec7 100644 --- a/step2-06/demo/src/components/TodoListItem.tsx +++ b/step2-06/demo/src/components/TodoListItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react'; import { actions } from '../actions'; -import { StoreContext } from 'redux-react-hook'; import { Store } from '../store'; import { connect } from 'react-redux'; @@ -69,7 +68,7 @@ class TodoListItem extends React.Component }; private onDoneEdit = () => { - this.context.dispatch(actions.edit(this.props.id, this.state.editLabel)); + this.props.edit(this.props.id, this.state.editLabel); this.setState({ editing: false, editLabel: undefined diff --git a/step2-06/exercise/README.md b/step2-06/exercise/README.md index 8638161..0cb2da9 100644 --- a/step2-06/exercise/README.md +++ b/step2-06/exercise/README.md @@ -2,16 +2,20 @@ [Lessons](../) | [Exercise](./exercise/) | [Demo](./demo/) -## Bind Redux Store with Class Component +If you haven't arStart the app by running `npm start` from the root of the `frontend-bootcamp` folder. Click the "exercise" link under day 2 step 6 to see results. -1. Open `exercise/src/components/TodoHeader.tsx`. +At the beginning of this exercise, the "Add" and "Clear Completed" buttons do not work. We'll be fixing that in this step! -2. Just like the 2.4 exercise, implement `onAdd` and `onFilter` using `this.context.dispatch()` calls to dispatch actions. +1. Open `exercise/src/index.tsx` and wrap `` with `` as instructed in the comment -## Bind Redux Store with Functional Component +2. Open `exercise/src/components/TodoFooter.tsx` and erase the "nullable" type modifier (i.e. the ?) in the interface definition of `TodoFooterProps` -1. Open `exercise/src/components/TodoFooter.tsx`. +3. Uncomment the bottom bits of code and fill in `connect()` arguments - feel free to use `TodoListItem.tsx` as a guide -2. Follow the instructions in the file to replace the `todos` const using the `useMappedState()` hook. +4. Repeat steps 2, 3 for the `TodoHeader.tsx` file -3. Retrieve the dispatch function with `useDispatch()` hook. +## Bonus exercise + +For further reading, go here to learn more about the `mergeProps` and `options` parameters to `connect()`: + +https://react-redux.js.org/api/connect diff --git a/step2-06/exercise/src/components/TodoFooter.tsx b/step2-06/exercise/src/components/TodoFooter.tsx index fc5307e..576a644 100644 --- a/step2-06/exercise/src/components/TodoFooter.tsx +++ b/step2-06/exercise/src/components/TodoFooter.tsx @@ -1,15 +1,18 @@ import React from 'react'; import { DefaultButton, Stack, Text } from 'office-ui-fabric-react'; import { actions } from '../actions'; -import { useMappedState, useDispatch } from 'redux-react-hook'; +import { connect } from 'react-redux'; +import { Store } from '../store'; -export const TodoFooter = () => { - // TODO: make use of useMappedState(state => state) and the useDispatch functions to get - // the Redux store and dispatching actions - // HINT: const { todos } = useMappedState(...); - // HINT: useDispatch() here too. - const todos = {}; - const dispatch = (...args: any[]) => {}; +// TODO: these ?'s after the keys of an interface makes it optional +// and can be removed when you finished connecting this component +interface TodoFooterProps { + todos?: Store['todos']; + clear?: () => void; +} + +const TodoFooter = (props: TodoFooterProps) => { + const { todos, clear } = props; const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length; @@ -18,7 +21,23 @@ export const TodoFooter = () => { {itemCount} item{itemCount === 1 ? '' : 's'} left - dispatch(actions.clear())}>Clear Completed + clear()}>Clear Completed ); }; + +// TODO: write out the mapping functions for state and dispatch functions +/* + HINT: you can get started by copy pasting below code as arguments to connect() + + (state: Store) => ({ + // TODO: mapping for state + // HINT: look at what the component needed from the props interface + }), + dispatch => ({ + // TODO: mapping for dispatch actions + // HINT: look at what the component needed from the props interface + }) +*/ +const ConnectedTodoFooter = connect()(TodoFooter); +export { ConnectedTodoFooter as TodoFooter }; diff --git a/step2-06/exercise/src/components/TodoHeader.tsx b/step2-06/exercise/src/components/TodoHeader.tsx index e447dad..3a1c288 100644 --- a/step2-06/exercise/src/components/TodoHeader.tsx +++ b/step2-06/exercise/src/components/TodoHeader.tsx @@ -2,14 +2,21 @@ import React from 'react'; import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react'; import { FilterTypes } from '../store'; import { actions } from '../actions'; -import { StoreContext } from 'redux-react-hook'; +import { connect } from 'react-redux'; + +// TODO: these ?'s after the keys of an interface makes it optional +// and can be removed when you finished connecting this component +interface TodoHeaderProps { + addTodo?: (label: string) => void; + setFilter?: (filter: FilterTypes) => void; +} interface TodoHeaderState { labelInput: string; } -export class TodoHeader extends React.Component<{}, TodoHeaderState> { - constructor(props: {}) { +class TodoHeader extends React.Component { + constructor(props: TodoHeaderProps) { super(props); this.state = { labelInput: undefined }; } @@ -49,8 +56,7 @@ export class TodoHeader extends React.Component<{}, TodoHeaderState> { } private onAdd = () => { - // TODO: Fill in a dispatch call to add the todo item - // HINT: this.context.dispatch(...); + this.props.addTodo(this.state.labelInput); this.setState({ labelInput: undefined }); }; @@ -59,9 +65,22 @@ export class TodoHeader extends React.Component<{}, TodoHeaderState> { }; private onFilter = (item: PivotItem) => { - // TODO: Fill in the dispatch call to set the filter - // HINT: this.context.dispatch(...); + this.props.setFilter(item.props.headerText as FilterTypes); }; } -// TODO: set the context type of this Class to StoreContext +// TODO: write out the mapping functions for state and dispatch functions +/* + HINT: you can get started by copy pasting below code as arguments to connect() + + (state: Store) => ({ + // TODO: mapping for state + // HINT: look at what the component needed from the props interface + }), + dispatch => ({ + // TODO: mapping for dispatch actions + // HINT: look at what the component needed from the props interface + }) +*/ +const ConnectedTodoHeader = connect()(TodoHeader); +export { ConnectedTodoHeader as TodoHeader }; diff --git a/step2-06/exercise/src/components/TodoList.tsx b/step2-06/exercise/src/components/TodoList.tsx index b577baa..02cad3f 100644 --- a/step2-06/exercise/src/components/TodoList.tsx +++ b/step2-06/exercise/src/components/TodoList.tsx @@ -1,10 +1,16 @@ import React from 'react'; import { Stack } from 'office-ui-fabric-react'; import { TodoListItem } from './TodoListItem'; -import { useMappedState } from 'redux-react-hook'; +import { connect } from 'react-redux'; +import { Store } from '../store'; -export const TodoList = () => { - const { filter, todos } = useMappedState(state => state); +interface TodoListProps { + todos: Store['todos']; + filter: Store['filter']; +} + +const TodoList = (props: TodoListProps) => { + const { filter, todos } = props; const filteredTodos = Object.keys(todos).filter(id => { return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed); }); @@ -17,3 +23,6 @@ export const TodoList = () => { ); }; + +const ConnectedTodoList = connect((state: Store) => ({ ...state }))(TodoList); +export { ConnectedTodoList as TodoList }; diff --git a/step2-06/exercise/src/components/TodoListItem.tsx b/step2-06/exercise/src/components/TodoListItem.tsx index e105851..2ac0ec7 100644 --- a/step2-06/exercise/src/components/TodoListItem.tsx +++ b/step2-06/exercise/src/components/TodoListItem.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react'; import { actions } from '../actions'; -import { StoreContext } from 'redux-react-hook'; +import { Store } from '../store'; +import { connect } from 'react-redux'; interface TodoListItemProps { id: string; + todos: Store['todos']; + complete: (id: string) => void; + remove: (id: string) => void; + edit: (id: string, label: string) => void; } interface TodoListItemState { @@ -12,7 +17,7 @@ interface TodoListItemState { editLabel: string; } -export class TodoListItem extends React.Component { +class TodoListItem extends React.Component { constructor(props: TodoListItemProps) { super(props); this.state = { editing: false, editLabel: undefined }; @@ -63,7 +68,7 @@ export class TodoListItem extends React.Component { - this.context.dispatch(actions.edit(this.props.id, this.state.editLabel)); + this.props.edit(this.props.id, this.state.editLabel); this.setState({ editing: false, editLabel: undefined @@ -75,4 +80,13 @@ export class TodoListItem extends React.Component ({ todos: state.todos }), + dispatch => ({ + complete: label => dispatch(actions.addTodo(label)), + remove: label => dispatch(actions.addTodo(label)), + edit: filter => dispatch(actions.setFilter(filter)) + }) +)(TodoListItem); + +export { ConnectedTodoListItem as TodoListItem }; diff --git a/step2-06/exercise/src/index.tsx b/step2-06/exercise/src/index.tsx index d994e52..c4d04eb 100644 --- a/step2-06/exercise/src/index.tsx +++ b/step2-06/exercise/src/index.tsx @@ -5,15 +5,11 @@ import { createStore } from 'redux'; import { TodoApp } from './components/TodoApp'; import { initializeIcons } from '@uifabric/icons'; import { composeWithDevTools } from 'redux-devtools-extension'; -import { StoreContext } from 'redux-react-hook'; +// TODO: import { Provider } from 'react-redux'; const store = createStore(reducer, {}, composeWithDevTools()); initializeIcons(); -ReactDOM.render( - - - , - document.getElementById('app') -); +// TODO: wrap the component with a component +ReactDOM.render(, document.getElementById('app')); diff --git a/step2-07/demo/src/components/TodoFooter.tsx b/step2-07/demo/src/components/TodoFooter.tsx index b92c239..ec5de8c 100644 --- a/step2-07/demo/src/components/TodoFooter.tsx +++ b/step2-07/demo/src/components/TodoFooter.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { DefaultButton, Stack, Text } from 'office-ui-fabric-react'; import { actionsWithService } from '../actions'; -import { useMappedState, useDispatch } from 'redux-react-hook'; +import { connect } from 'react-redux'; +import { Store } from '../store'; -export const TodoFooter = () => { - const { todos } = useMappedState(state => state); - const dispatch = useDispatch(); +interface TodoFooterProps { + todos: Store['todos']; + clear: () => void; +} + +const TodoFooter = (props: TodoFooterProps) => { + const { todos, clear } = props; const itemCount = Object.keys(todos).filter(id => !todos[id].completed).length; @@ -14,7 +19,18 @@ export const TodoFooter = () => { {itemCount} item{itemCount === 1 ? '' : 's'} left - dispatch(actionsWithService.clear())}>Clear Completed + clear()}>Clear Completed ); }; + +const ConnectedTodoFooter = connect( + (state: Store) => ({ + todos: state.todos + }), + (dispatch: any) => ({ + clear: () => dispatch(actionsWithService.clear()) + }) +)(TodoFooter); + +export { ConnectedTodoFooter as TodoFooter }; diff --git a/step2-07/demo/src/components/TodoHeader.tsx b/step2-07/demo/src/components/TodoHeader.tsx index 871febc..997b4db 100644 --- a/step2-07/demo/src/components/TodoHeader.tsx +++ b/step2-07/demo/src/components/TodoHeader.tsx @@ -1,15 +1,20 @@ import React from 'react'; import { Stack, Text, Pivot, PivotItem, TextField, PrimaryButton } from 'office-ui-fabric-react'; import { FilterTypes } from '../store'; -import { actionsWithService, actions } from '../actions'; -import { StoreContext } from 'redux-react-hook'; +import { actions, actionsWithService } from '../actions'; +import { connect } from 'react-redux'; + +interface TodoHeaderProps { + addTodo: (label: string) => void; + setFilter: (filter: FilterTypes) => void; +} interface TodoHeaderState { labelInput: string; } -export class TodoHeader extends React.Component<{}, TodoHeaderState> { - constructor(props: {}) { +class TodoHeader extends React.Component { + constructor(props: TodoHeaderProps) { super(props); this.state = { labelInput: undefined }; } @@ -49,7 +54,7 @@ export class TodoHeader extends React.Component<{}, TodoHeaderState> { } private onAdd = () => { - this.context.dispatch(actionsWithService.addTodo(this.state.labelInput)); + this.props.addTodo(this.state.labelInput); this.setState({ labelInput: undefined }); }; @@ -58,8 +63,16 @@ export class TodoHeader extends React.Component<{}, TodoHeaderState> { }; private onFilter = (item: PivotItem) => { - this.context.dispatch(actions.setFilter(item.props.headerText as FilterTypes)); + this.props.setFilter(item.props.headerText as FilterTypes); }; } -TodoHeader.contextType = StoreContext; +const ConnectedTodoHeader = connect( + state => {}, + (dispatch: any) => ({ + addTodo: label => dispatch(actionsWithService.addTodo(label)), + setFilter: filter => dispatch(actions.setFilter(filter)) + }) +)(TodoHeader); + +export { ConnectedTodoHeader as TodoHeader }; diff --git a/step2-07/demo/src/components/TodoList.tsx b/step2-07/demo/src/components/TodoList.tsx index b577baa..02cad3f 100644 --- a/step2-07/demo/src/components/TodoList.tsx +++ b/step2-07/demo/src/components/TodoList.tsx @@ -1,10 +1,16 @@ import React from 'react'; import { Stack } from 'office-ui-fabric-react'; import { TodoListItem } from './TodoListItem'; -import { useMappedState } from 'redux-react-hook'; +import { connect } from 'react-redux'; +import { Store } from '../store'; -export const TodoList = () => { - const { filter, todos } = useMappedState(state => state); +interface TodoListProps { + todos: Store['todos']; + filter: Store['filter']; +} + +const TodoList = (props: TodoListProps) => { + const { filter, todos } = props; const filteredTodos = Object.keys(todos).filter(id => { return filter === 'all' || (filter === 'completed' && todos[id].completed) || (filter === 'active' && !todos[id].completed); }); @@ -17,3 +23,6 @@ export const TodoList = () => { ); }; + +const ConnectedTodoList = connect((state: Store) => ({ ...state }))(TodoList); +export { ConnectedTodoList as TodoList }; diff --git a/step2-07/demo/src/components/TodoListItem.tsx b/step2-07/demo/src/components/TodoListItem.tsx index 71d9b1e..947feaa 100644 --- a/step2-07/demo/src/components/TodoListItem.tsx +++ b/step2-07/demo/src/components/TodoListItem.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { Stack, Checkbox, IconButton, TextField, DefaultButton } from 'office-ui-fabric-react'; -import { actionsWithService } from '../actions'; -import { StoreContext } from 'redux-react-hook'; +import { actions, actionsWithService } from '../actions'; +import { Store } from '../store'; +import { connect } from 'react-redux'; interface TodoListItemProps { id: string; + todos: Store['todos']; + complete: (id: string) => void; + remove: (id: string) => void; + edit: (id: string, label: string) => void; } interface TodoListItemState { @@ -12,7 +17,7 @@ interface TodoListItemState { editLabel: string; } -export class TodoListItem extends React.Component { +class TodoListItem extends React.Component { constructor(props: TodoListItemProps) { super(props); this.state = { editing: false, editLabel: undefined }; @@ -29,10 +34,10 @@ export class TodoListItem extends React.Component {!this.state.editing && ( <> - dispatch(actionsWithService.complete(id))} /> + dispatch(actions.complete(id))} />
    - dispatch(actionsWithService.remove(id))} /> + dispatch(actions.remove(id))} />
    )} @@ -63,7 +68,7 @@ export class TodoListItem extends React.Component { - this.context.dispatch(actionsWithService.edit(this.props.id, this.state.editLabel)); + this.props.edit(this.props.id, this.state.editLabel); this.setState({ editing: false, editLabel: undefined @@ -75,4 +80,13 @@ export class TodoListItem extends React.Component ({ todos: state.todos }), + (dispatch: any) => ({ + complete: label => dispatch(actionsWithService.addTodo(label)), + remove: label => dispatch(actionsWithService.addTodo(label)), + edit: filter => dispatch(actions.setFilter(filter)) + }) +)(TodoListItem); + +export { ConnectedTodoListItem as TodoListItem }; diff --git a/step2-07/demo/src/index.tsx b/step2-07/demo/src/index.tsx index eadd502..4cc9e8c 100644 --- a/step2-07/demo/src/index.tsx +++ b/step2-07/demo/src/index.tsx @@ -3,9 +3,9 @@ import ReactDOM from 'react-dom'; import { reducer } from './reducers'; import { createStore, applyMiddleware } from 'redux'; import { TodoApp } from './components/TodoApp'; +import { Provider } from 'react-redux'; import { initializeIcons } from '@uifabric/icons'; import { composeWithDevTools } from 'redux-devtools-extension'; -import { StoreContext } from 'redux-react-hook'; import thunk from 'redux-thunk'; import { FilterTypes } from './store'; @@ -22,9 +22,9 @@ import { FilterTypes } from './store'; initializeIcons(); ReactDOM.render( - + - , +
    , document.getElementById('app') ); })();