React App Kata 6 TypeScript
July 13, 2018 ยท View on GitHub
Code for Kata 6 is available in the app-ts-6 folder.
Learning aims
The idea here is to start using Redux for our app and understand it's main concepts.
Requirements
Get started
You will need 2 terminals
-
Web API server
- go to
./app-ts-6 - verify dotnet version
dotnet --versionis higher than2.0.0 - run
dotnet build - run
dotnet run
This should build the web api server and serve it at
http:\\localhost:5000 - go to
-
Web app
- in another terminal
- go to
./app-ts-6/app/ - follow the instructions in the README.
- your app should be running at port 3000
Concepts
Let's go through some basic concepts before we start. We have added a simple end to end React Redux example in the code of app-ts-6 that we will explain first.
-
Actions
Actions are sets of information that are sent to your application. An action has a
type(mandatory) and apayload(optional).Example of an action with no payload fired:
dispatch(VersionActions.versionRequested());Example of an action with payload fired:
dispatch(VersionActions.versionCompleted(version));Here is an example of the definition of
versionRequestedan actions:export const VersionActions = { versionRequested: createAction(TypeKeys.WEB_SERVER_VERSION_REQUESTED, () => ({ type: TypeKeys.WEB_SERVER_VERSION_REQUESTED })), }; -
Reducers
Reducers define how the state of our app should change with respect to actions that are fired.
All of our reducers are combined in
/src/modules/index.tsimport { combineReducers } from 'redux'; import versions, { VersionsState, VersionActionTypes } from './versions'; interface StoreEnhancerState { } export interface RootState extends StoreEnhancerState { versions: VersionsState; } export default combineReducers<RootState>({ versions, }); type AppAction = | VersionActionTypes; export type RootAction = AppAction;Our
versionsreducer shows examples of how the state of our app changed based on the actions that are fired:/src/modules/versions.tsconst createEmptyMember = (): VersionsState => ({ inProgress: false, version: undefined, error: undefined, }); export default (state = createEmptyMember(), action: VersionActionTypes) => { switch (action.type) { case TypeKeys.WEB_SERVER_VERSION_REQUESTED: return { ...state, inProgress: true, }; case TypeKeys.WEB_SERVER_VERSION_COMPLETED: return { ...state, inProgress: false, version: action.payload.version }; case TypeKeys.WEB_SERVER_VERSION_FAILED: return { ...state, inProgress: false, version: undefined, error: action.payload.error }; default: return state; } };Note: Everytime you return an object in the
casestatement of the reducer you are defining the new state of that section of the application.This means that when you update the state of any reducer you need to do it without changing the exisitng state. I.e. use a new object instead of an the existing one.
That is the reason why we use the spread operator when returning the new state in each case.
case WEB_SERVER_VERSION_REQUESTED: return { ...state, inProgress: true } -
Store
The store contains all the state of your application.
Ours is defined at
/src/store.jsconst initialState = {} const store: Store<any> = createStore( rootReducer, initialState, composedEnhancers ); export default store;It already uses a
rootReducerwhere our app defines the reducers it needs under/src/modules/index.tsimport rootReducer from './modules' -
give access to the store to all components
To do this we inject in our
src/index.tsthestoreusing theProvidercomponent. For more information see: Passing the Storeimport * as React from 'react'; import * as ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { ConnectedRouter } from 'react-router-redux'; import { Provider } from 'react-redux'; import store, { history } from './store'; ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, document.getElementById('root') ); -
create a dispatch function for fetching versions
-
we define a
fetchWebServerVersionfunction in/src/modules/versions.jsfunction fetchWebServerVersion() { const url = '/api/versions/get'; return fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'same-origin' }).then(response => { if (response.ok) { return response.json(); } const error = new Error(response.statusText); throw error; }); } type VersionThunkAsync = ActionCreator<ThunkAction<Promise<void>, VersionsState, void>>; export const fetchVersion: VersionThunkAsync = () => { return async (dispatch: Dispatch<VersionsState>): Promise<void> => { dispatch(VersionActions.versionRequested()); await fetchWebServerVersion() .then((version: string) => dispatch(VersionActions.versionCompleted(version))) .catch(e => { dispatch(VersionActions.versionFailed(e.error)); }); }; };Things to notice here:
fetchWebServerVersionfunction returns a Promise.- we dispatch
WEB_SERVER_VERSION_REQUESTEDwithdispatch(VersionActions.versionRequested())action before the fetch call - we dispatch
WEB_SERVER_VERSION_COMPLETEDwithdispatch(VersionActions.versionCompleted(version))action when the fetch call completes
-
-
Connect your dispatch function and the data present in the store to the
Appcomponent-
we import fetch function and connect state to props
import { Dispatch, bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { RootState, RootAction } from './modules'; import { fetchVersion } from './modules/versions'; const mapStateToProps = (state: RootState) => ({ version: state.versions.version, }); const mapDispatchToProps = (dispatch: Dispatch<RootAction>) => bindActionCreators( { fetchVersion, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps, null, { pure: false })(App);Remark 1: Notice the line
version: state.versions.versionwhere we connect the data from the store as a property of the component so that it can be used asthis.props.versiondirectly.Remark 2: Notice the
mapDispatchToProps, where we add thefetchWebServerVersionfunction. So that we can callthis.props.fetchWebServerVersionand that all calls withdispatchfor exampledispatch(VersionActions.versionRequested())get properly fired. -
use the version data present in the
storein ourAppcomponent<div className='App-header'> <h2>Kata 6 - TypeScript - Redux</h2> <pre>v{this.props.version}</pre> </div>
-
-
Check redux actions in the app
The store we created in
src/store.tshas aloggeras one of our middleware as well asreact-router-redux. This will allow us to check the actions that are fired within the app.import logger from 'redux-logger'; const middleware = [ thunk, logger, routerMiddleware(history) ];If you check you browser console you should see the actions and the change of each state in the store.

-
The versions example
Throughout the Redux concepts we showed how the versions request example work in this app. Have a look at the different pieces to understand how they work:
src/index.tssrc/App.tssrc/store.tssrc/modules/versions.tssrc/modules/index.ts
Task
Redux is alredy installed and working in this application. Your goal is to switch the app code into using Redux.
Write the TypeScript/JavaScript/React code to avoid any use of state in the App.tsx component. Use redux instead
- move all api calls into the
productsreducer:- under
src/modulescreate file calledproducts.tsand export an empty reducer. - Connect the reducter to the store by updating
index.ts. - create the action types for products in
products.tsproducts/PRODUCTS_REQUESTEDproducts/PRODUCTS_COMPLETEDproducts/PRODUCT_ADD_REQUESTEDproducts/PRODUCT_ADD_COMPLETEDproducts/PRODUCT_REMOVE_REQUESTEDproducts/PRODUCT_REMOVE_COMPLETED
- create and implement all api functions that currently exist in
App.tsxfetchProductsaddProduct(passingnewProductas argument)removeProduct(passingproductNameas argument)
- be sure each function dispatches a
requestedaction before calling the server, and acompletedaction if the request succeeds
- under
- update the products reducer so that the data of the products list is updated acordingly:
- create and implement a case for each action type (requested and completed). Set the products returned from server (if applicable)
- import all dispatch product functions into
App.tsx- be sure to map them to props in the
mapDispatchToPropssection
- be sure to map them to props in the
- use the products data from the store
- be sure to map the products data in the
mapStateToPropssection
- be sure to map the products data in the
- update your
App.tsxcode so that you don't usethis.stateanymore. You shoul be usingthis.propsinstead - check in the browser console that all actions are being fired