diff --git a/SUMMARY.md b/SUMMARY.md index 59ebd73..fd99e92 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -24,10 +24,13 @@ * [Reducer](/docs/REDUX.md#reducer) * [Store](/docs/REDUX.md#store) * [Views](/docs/VIEWS.md) - * [Begin](/docs/VIEWS.md#begin) + * [IndexView](/docs/VIEWS.md#indexview) * [Alternatives](/docs/VIEWS.md#alternatives) +* [Reducers](/docs/REDUCERS.md) + * [IndexReducer](/docs/REDUCERS.md#indexreducer) + * [Connecting the reducer](/docs/REDUCERS.md#connecting) * [Containers](/docs/CONTAINERS.md) * [Initialize](/docs/CONTAINERS.md#initialize) - * [Begin](/docs/CONTAINERS.md#begin) + * [IndexContainer](/docs/CONTAINERS.md#indexcontainer) * [Styles](/docs/STYLES.md) * [Tests](/docs/TESTS.md) diff --git a/docs/CONTAINERS.md b/docs/CONTAINERS.md index 4ec8c0c..2996a2c 100644 --- a/docs/CONTAINERS.md +++ b/docs/CONTAINERS.md @@ -13,7 +13,7 @@ and the type definitions for it yarn add -D @types/react-redux ``` -### Begin +### IndexContainer We begin by creating a file called `IndexContainer.ts` inside our `index`-folder inside `src/modules` > All containers will have the same "prefix" as their accompanied **views**, a.k.a. `[Pagename]Container.ts` diff --git a/docs/REDUCERS.md b/docs/REDUCERS.md new file mode 100644 index 0000000..54c44d6 --- /dev/null +++ b/docs/REDUCERS.md @@ -0,0 +1,346 @@ +# Reducers + +Now we build our business logic, also known as **reducers**. Stay calm, this will be a bit more complicated then anything before. + +### IndexReducer + +We begin by creating a file called `IndexReducer.ts` in the `src/modules/index`-folder +> Our **reducers** follow the naming pattern of `[Foldername]Reducer` + +```typescript +import { MiddlewareAPI } from 'redux'; +import { Observable } from 'rxjs/observable'; +import { ajax } from 'rxjs/observable/dom/ajax'; +import 'rxjs/add/operator/delay'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/mapTo'; +import { Epic, combineEpics, ActionsObservable } from 'redux-observable'; +import { DefaultAction } from '../../redux/utils'; +import Todo from '../../common/Todo'; + +export class IndexState { + readonly title: string = ''; + readonly todos: Todo[] = []; + readonly loading: boolean = false; +} + +type SET_TITLE = 'boilerplate/Index/SET_TITLE'; +const SET_TITLE: SET_TITLE = 'boilerplate/Index/SET_TITLE'; +type SAVE_TODO = 'boilerplate/Index/SAVE_TODO'; +const SAVE_TODO: SAVE_TODO = 'boilerplate/Index/SAVE_TODO'; +type SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS'; +const SAVE_TODO_SUCCESS: SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS'; +type SET_DONE = 'boilerplate/Index/SET_DONE'; +const SET_DONE: SET_DONE = 'boilerplate/Index/SET_DONE'; +type SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS'; +const SET_DONE_SUCCESS: SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS'; + +type SetTitleAction = { type: SET_TITLE, payload: string }; +export const setTitle = (title: string): SetTitleAction => ({ type: SET_TITLE, payload: title }); +type SaveTodoAction = { type: SAVE_TODO }; +export const saveTodo = (): SaveTodoAction => ({ type: SAVE_TODO }); +type SaveTodoSuccessAction = { type: SAVE_TODO_SUCCESS }; +const saveTodoSuccess = (): SaveTodoSuccessAction => ({ type: SAVE_TODO_SUCCESS }); +type SetDoneAction = { type: SET_DONE, payload: number }; +export const setDone = (i: number) => ({ type: SET_DONE, payload: i }); +type SetDoneSuccessAction = { type: SET_DONE_SUCCESS, payload: number }; +const setDoneSuccess = (i: number): SetDoneSuccessAction => ({ type: SET_DONE_SUCCESS, payload: i }); + +export type IndexActions = SetTitleAction | SaveTodoAction | SaveTodoSuccessAction | SetDoneAction | SetDoneSuccessAction | DefaultAction; + +const saveTodoEpic: Epic = (action$: ActionsObservable): Observable => + action$.ofType(SAVE_TODO) + .delay(1000) + .mapTo(saveTodoSuccess()); + +const setDoneEpic: Epic = (action$: ActionsObservable): Observable => + action$.ofType(SET_DONE) + .delay(1000) + .map((action: SetDoneAction) => setDoneSuccess(action.payload)); + +export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic); + +const IndexReducer = (state: IndexState = new IndexState(), action: IndexActions = DefaultAction): IndexState => { + switch (action.type) { + case SET_TITLE: + return { ...state, title: action.payload }; + case SAVE_TODO: + return { ...state, loading: true }; + case SAVE_TODO_SUCCESS: + return { + ...state, + title: '', + todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)), + loading: false, + }; + case SET_DONE: + return { ...state, loading: true }; + case SET_DONE_SUCCESS: + return { + ...state, + todos: state.todos.map(t => t.id === action.payload ? t.setDone() : t), + loading: false, + }; + default: + return state; + } +}; + +export default IndexReducer; +``` + +--- + +First we define the **state** for our `IndexReducer` +> **Reducers** usually define their own **state** and we'll show you [later](#connecting) how to connect it to the main **reducer** and **state** + +```typescript +import Todo from '../../common/Todo'; + +export class IndexState { + readonly title: string = ''; + readonly todos: Todo[] = []; + readonly loading: boolean = false; +} +``` +which is fairly simple. Here we define the `IndexState` as a class, with the given properties (*make sure you add default values for required properties so you can instantiate it!*), with the `title` for the current `Todo` the user is creating, `todos` for the list of current `Todo`s and `loading` to show the user whether the application is performing an async call or not. + +--- + +Next up we define our [action types](http://redux.js.org/docs/basics/Actions.html) +```typescript +type SET_TITLE = 'boilerplate/Index/SET_TITLE'; +const SET_TITLE: SET_TITLE = 'boilerplate/Index/SET_TITLE'; +type SAVE_TODO = 'boilerplate/Index/SAVE_TODO'; +const SAVE_TODO: SAVE_TODO = 'boilerplate/Index/SAVE_TODO'; +type SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS'; +const SAVE_TODO_SUCCESS: SAVE_TODO_SUCCESS = 'boilerplate/Index/SAVE_TODO_SUCCESS'; +type SET_DONE = 'boilerplate/Index/SET_DONE'; +const SET_DONE: SET_DONE = 'boilerplate/Index/SET_DONE'; +type SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS'; +const SET_DONE_SUCCESS: SET_DONE_SUCCESS = 'boilerplate/Index/SET_DONE_SUCCESS'; +``` +which **redux** recommends to be constant `string`s. The reason we first define a `type` for the action type is to get **TypeScript** to do [some of the work for us](https://spin.atomicobject.com/2016/09/27/typed-redux-reducers-typescript-2-0/), by ensuring that each instance of the action type has the same value, so for example a constant of `SET_TITLE: SET_TITLE = 'notcorrect'` would raise a compiler error from **TypeScript** as it is not equal to `boilerplate/Index/SET_TITLE`. +> Here we follow the [redux-ducks](https://github.com/erikras/ducks-modular-redux) naming pattern of the format `applicationName/ViewName/ACTION_TYPE` + +--- + +Next we define our **action creators** which are functions that return an **action** +```typescript +type SetTitleAction = { type: SET_TITLE, payload: string }; +export const setTitle = (title: string): SetTitleAction => ({ type: SET_TITLE, payload: title }); +type SaveTodoAction = { type: SAVE_TODO }; +export const saveTodo = (): SaveTodoAction => ({ type: SAVE_TODO }); +type SaveTodoSuccessAction = { type: SAVE_TODO_SUCCESS }; +const saveTodoSuccess = (): SaveTodoSuccessAction => ({ type: SAVE_TODO_SUCCESS }); +type SetDoneAction = { type: SET_DONE, payload: number }; +export const setDone = (i: number) => ({ type: SET_DONE, payload: i }); +type SetDoneSuccessAction = { type: SET_DONE_SUCCESS, payload: number }; +const setDoneSuccess = (i: number): SetDoneSuccessAction => ({ type: SET_DONE_SUCCESS, payload: i }); +``` +of a specific type with a specific `payload` (*which is the way to pass new information to the **reducer***). In this case we also first define the `type` for each of the possible **actions** at the same time as the **action creator**. + +--- + +After defining our **actions** and **action creators** we also create a combined type of all of them +```typescript +import { DefaultAction } from '../../redux/utils'; + +export type IndexActions = SetTitleAction | SaveTodoAction | SaveTodoSuccessAction | SetDoneAction | SetDoneSuccessAction | DefaultAction; +``` +to allow us to later define when we want to receive an **action** for our `Index`-page and later combine them to create a shared type for all the **actions** in our application. + +--- + +Next we define our [Epics](https://redux-observable.js.org/docs/basics/Epics.html) +```typescript +import { Observable } from 'rxjs/observable'; +import 'rxjs/add/operator/delay'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/mapTo'; +import { Epic, combineEpics, ActionsObservable } from 'redux-observable'; + +const saveTodoEpic: Epic = (action$: ActionsObservable): Observable => + action$.ofType(SAVE_TODO) + .delay(1000) + .mapTo(saveTodoSuccess()); + +const setDoneEpic: Epic = (action$: ActionsObservable): Observable => + action$.ofType(SET_DONE) + .delay(1000) + .map((action: SetDoneAction) => setDoneSuccess(action.payload)); + +export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic); +``` +which are [redux-observable's](https://redux-observable.js.org) way of handling side-effects in **Redux** (*like AJAX calls etc.*). At the end we combine all our **Epics** in this file to a single exportable **Epic** called `IndexEpics` (*so we only need to import one variable when we want access to these later*). +> The importing part may look a little weird, but it's because [RxJS](http://reactivex.io/rxjs/) is a rather large library, we can either import everything using `import * as RxJS from 'rxjs'` or import only the parts we need as shown above, which will allow any proper [minifier](https://developers.google.com/speed/docs/insights/MinifyResources) like [UglifyJS](https://github.com/mishoo/UglifyJS) include only the needed parts from **RxJS** + +The first line +```typescript +const saveTodoEpic: Epic = (action$: ActionsObservable): Observable => +``` +defines an **Epic** which takes in as the first type argument the `type` for the `Actions` the epic takes in (*and returns*), in this case `IndexActions` which we defined earlier, and as the second argument the type of the **State** it takes in (*which isn't needed this time, so undefined will do*). An **Epic** is a function that takes in a [stream](https://en.wikipedia.org/wiki/Stream_(computing)) (*in this case of the type `ActionsObservable`*) which includes items of the `type` given as the first type argument and returns another stream (*in this case an [`Observable`](http://reactivex.io/documentation/observable.html), which `ActionsObservable` is based on*) which includes items of the same `type` as the input stream. +> In JavaScript the convention is to append a `$` to all variables names that are **streams**, to let the developer know that they are dealing with one + +The second line +```typescript + action$.ofType(SET_DONE) +``` +utilizes the inbuilt function `ofType(key: string)` of `ActionsObservable`, which basically filters out all **actions** that do not have the `type`-property of the given argument. +> A more verbose, but maybe a simpler to understand version would be to write +```typescript +action$.filter(action => action.type === SET_DONE) +``` +> If you find yourself needing to understand the types of the components provided by **redux-observable** I suggest reading [this](https://github.com/redux-observable/redux-observable/blob/master/index.d.ts) + +The third and fourth line +```typescript + .delay(1000) + .mapTo(saveTodoSuccess()); +``` +include the actual functionality of our **Epic**. In this case **after** we receive an **action** of the type `SET_DONE` we wait for 1 second (*`delay`takes milliseconds as argument*) and then we return an **action** of the type `SAVE_TODO_SUCCESS` (*in this case using [`mapTo`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mapTo) as we just want to return a new **Action***). +> If you wanted to return multiple actions, say `SET_DONE_SUCCESS` and an imaginary `SEND_PUSH_NOTIFICATION` you could do it using [`mergeMap`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeMap), which is kind of like `flatMap`, like so: +```typescript +import 'rxjs/add/observable/from'; +import 'rxjs/add/operator/mergeMap'; +... + action$.ofType(SET_DONE) + .mergeMap((action: SetDoneAction) => Observable.from([ + setDoneSuccess(action.payload), + // SEND_PUSH_NOTIFICATION, + // OTHER ACTIONS, + ])); +``` + +The other **Epic** is otherwirse similar, but it uses [`map`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-map) +```typescript + .delay(1000) + .map((action: SetDoneAction) => setDoneSuccess(action.payload)); +``` +to return an action of the type `SET_DONE_SUCCESS`, using the payload of the incoming action. + +> If you wanted to do an AJAX call, you would go about it like this: +```typescript +import { ajax } from 'rxjs/observable/dom/ajax'; +... + action$.ofType(AJAX_CALL).mergeMap((action: AjaxCallAction) => + // For a get JSON call + ajax.getJSON('url', { headers: 'go here' }) + .map(response => someAction(response)) + .catch(err => errorAction(err))); + // For all other calls, just select the correct verb + ajax.post('url', payload, { headers: 'go here' }) + .map(response => someAction(response)) + .catch(err => errorAction(err)); +``` +> **Redux-observable** is built upon [RxJS](http://reactivex.io/), the JavaScript implemention of **ReactiveX** and most issues you will run into will be **RxJS** issues + +--- + +Finally we define the rest of our business logic, a.k.a. the **reducer** itself +```typescript +const IndexReducer = (state: IndexState = new IndexState(), action: IndexActions = DefaultAction): IndexState => { + switch (action.type) { + case SET_TITLE: + return { ...state, title: action.payload }; + case SAVE_TODO: + return { ...state, loading: true }; + case SAVE_TODO_SUCCESS: + return { + ...state, + title: '', + todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)), + loading: false, + }; + case SET_DONE: + return { ...state, loading: true }; + case SET_DONE_SUCCESS: + return { + ...state, + todos: state.todos.map(t => t.id === action.payload ? t.setDone() : t), + loading: false, + }; + default: + return state; + } +}; +``` +for which I suggest to break from the **redux-ducks** pattern by using the naming convention of `[Pagename]Reducer`. The important thing to remember with **reducers** is that they have to be [functional](https://en.wikipedia.org/wiki/Functional_programming), a.k.a. they are not allowed to mutate the incoming information. + +On the first line we define the signature of our `IndexReducer` +```typescript +const IndexReducer = (state: IndexState = new IndexState(), action: IndexActions = DefaultAction): IndexState => +``` +where we define it to take to parameters (*as all **reducers***), our `IndexState` (*with a default for the empty state*) and an action. `IndexReducer` will also return an `IndexState` (*as all **reducers***). + +Next we do the actual logic which all **reducers** are built upon +```typescript + switch (action.type) { + case SET_TITLE: + return { ...state, title: action.payload }; + case SAVE_TODO: + return { ...state, loading: true }; + case SAVE_TODO_SUCCESS: + return { + ...state, + title: '', + todos: state.todos.concat(new Todo(state.todos.length + 1, state.title)), + loading: false, + }; + case SET_DONE: + return { ...state, loading: true }; + case SET_DONE_SUCCESS: + return { + ...state, + todos: state.todos.map(t => t.id === action.payload ? t.setDone() : t), + loading: false, + }; + default: + return state; + } +``` +which is a [`switch`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/switch)-statement over the type-property of the incoming **action**. In each `case` we do something (*except the `default`-one, where you traditionally just return the incoming **state***) to add value to that **action**, such as setting the `title` to the `payload` in the **action** in case the **action** is a `SET_TITLE`-action. Notice how we are using the [spread syntax](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator) to immutably create a new version of the state, thus holding true to the immutability of **reducers**. +> Other options are to use [`Object.assign({}, ...)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) or [Immutable](https://facebook.github.io/immutable-js/) + +### Connecting the reducer + +Remember our [root-reducer](/REDUX.md#reducer)? Now we connect our `IndexReducer` to it. + +First the **reducer** itself +```typescript +import { combineReducers } from 'redux'; +import IndexReducer from '../modulex/index/IndexReducer'; + +const reducer = combineReducers({ + index: IndexReducer, +}); +``` +where we add the `IndexReducer` under the key `index`, which is very important, as when `combineReducers` combines included **reducers** it will put their specific state under the key given, in the global **state**-object. + +Next we add the `IndexState` to our global `State`-class (*this is just to allow us to define the type and initialize it for tests later on*) +```typescript +import { IndexState } from '../modules/index/IndexReducer'; + +export class State { + readonly index: IndexState = new IndexState(); +} +``` +where we define that the global `State`-object has a property `index` of the type `IndexState` (*as our `combineReducer` already says, but we want to be explicit here*). + +Then we want to add our **Epics** into the global `epics` constant +```typescript +import { combineEpics } from 'redux-observable'; +import { IndexEpics } from '../modules/index/IndexReducer'; + +export const epics = combineEpics(IndexEpics); +``` +by including it as a parameter to `combineEpics`. + +Finally we add our `IndexActions` to the global `Actions`-type +```typescript +import { DefaultAction } from './utils'; +import { IndexActions } from '../modules/index/IndexReducer'; + +export type Actions = DefaultAction | IndexActions; +``` +where we say that `Actions` is a type where the value is either of a type of `DefaultAction` or one of the `IndexActions`. diff --git a/docs/REDUX.md b/docs/REDUX.md index b77d61b..2da6969 100644 --- a/docs/REDUX.md +++ b/docs/REDUX.md @@ -23,7 +23,7 @@ export const DefaultAction: DefaultAction = { type: '' }; ### Reducer -Now we will define our root-reducer in a file called `store.ts` inside the folder `redux`: +Now we will define our root-reducer in a file called `reducer.ts` inside the folder `redux`: ```typescript import { combineReducers } from 'redux'; import { combineEpics } from 'redux-observable'; diff --git a/docs/VIEWS.md b/docs/VIEWS.md index 54709c6..06d4353 100644 --- a/docs/VIEWS.md +++ b/docs/VIEWS.md @@ -2,7 +2,7 @@ Now we are ready to start working towards the beef of the application: different pages (or views). -### Begin +### IndexView We will begin by creating a file called `IndexView.tsx` (*remember that 'x' in the end of the file type means that it contains [jsx](https://facebook.github.io/react/docs/jsx-in-depth.html)*) inside a folder called `index` inside the `components`-folder: > All of our pages will be inside folders named after the page, in this case **Index** and the view will be named `[Pagename]View.tsx` diff --git a/src/modules/index/IndexReducer.ts b/src/modules/index/IndexReducer.ts index ed3b7d2..8296c4a 100644 --- a/src/modules/index/IndexReducer.ts +++ b/src/modules/index/IndexReducer.ts @@ -1,6 +1,4 @@ -import { MiddlewareAPI } from 'redux'; import { Observable } from 'rxjs/observable'; -import { ajax } from 'rxjs/observable/dom/ajax'; import 'rxjs/add/operator/delay'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/mapTo'; @@ -50,6 +48,8 @@ export const setDoneEpic: Epic = (action$: ActionsObser .delay(testDelay) .map((action: SetDoneAction) => setDoneSuccess(action.payload)); +export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic); + const IndexReducer = (state: IndexState = new IndexState(), action: IndexActions = DefaultAction): IndexState => { switch (action.type) { case SET_TITLE: @@ -76,6 +76,4 @@ const IndexReducer = (state: IndexState = new IndexState(), action: IndexActions } }; -export const IndexEpics = combineEpics(saveTodoEpic, setDoneEpic); - export default IndexReducer;