Click here to Skip to main content
14,972,394 members
Articles / Web Development / React
Article
Posted 25 Jul 2020

Stats

6.8K views
1 bookmarked

Implementing Model View Update Pattern in Typescript

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
9 Aug 2020MIT22 min read
In this article, we will see how to build a small application in a functional way.
In this post, predictable state, selectors, actions, high order components, effects will be the main actors on the playground. Material from this article can be used to learn MVU. It outlines the impact of the functional approach on the shape of the implemented application.

The world of applications development is vast and rich. There are small blocks. There are bigger composite abstractions, different programming languages, libraries for the programming languages, new trends, fashions, forgotten treasures, and silent components that have kept unnoticed.

With this article, we will build a small application in a functional way. Predictable state, selectors, actions, high order components, effects will be the main actors on the playground. Material from this article can be used to learn MVU. It can be used as an experimental playground, and will challenge those who seek knowledge about functional development. It outlines the impact of the functional approach on the shape of the implemented application.

Still we keep inventing, re-inventing. Discover and rediscover. For me, everything has begun from the glory of WndProc. The message dispatching system in Windows API. Now the fashion gradually is moving towards functional development. Message dispatching queue, predictable state container, monad functions is the answer to the fashion.

When I know - I don't know What I don't know.

Model View Update (MVU) - Existing, Re-invented, Re-discovered

MVU is the design pattern that has gradually emerged from functional programming languages. In the earlier stages, Object Oriented Programming (OOP) was taking the first small steps. Existing developers used the functional paradigm, monads operator abstractions, functional map and reduce operators while resolving requirements. Let's try to follow the functional software path and resolve the task like it could be done with the modern technologies. What limits are there? Are those limits real? Maybe it was a trade deal in favour of OOP fashion.

Sub Layers in the Draft MVU

Since MVU is another design pattern. Similar to MVC, MVVM would have a similar high layer ecosystem such as data transfer, model, view layers. More about those layers can be found in the article Introducing MVVM Architecture. In this article, let's focus on the difference. The view layer has a different shape. Since at our responsibility just functions. The state of the application is going to be updated frequently. Only some state updates are related to UI changes. There should be a tool that would pick and update only a part of the UI that is actually changed. The rest of the UI will be the same. There is a modern concept to it. It is a Virtual DOM. Since the full vDom is a big task. Here will be just a small version. Enough, to show the general idea.

Virtual DOM

It is separated into three parts:

  1. Template - will keep markup that describes the shape of the View
  2. DOM - to work with browser elements
  3. Virtual DOM - to produce initial views and perform small updates

It would provide the method to locate elements on UI. Elements that are bound to an updated part of the state. From the start, the task sounds a bit strange. However, it has a clever solution. Let's check the implementation and try to understand the most important parts.

This is what could be done with the UI in terms of DOM entities:

JavaScript
const dom = {
    eventHandlers: [],
    closest(el, selector) {
        return el.closest(selector);
    },
    attach(inst, selector, eventName, fn) {
        const pair = dom.eventHandlers.find(([key]) => fn === key);
        const index = dom.eventHandlers.indexOf(pair);
        if (index >= 0) {
            throw new Error(`Event handler ${fn} for event ${eventName} already attached`);
        }
        let detach;
        const handler = function (evnt) {
            if (selector && dom.closest(evnt.target, selector)) {
                fn(evnt);
            } else if (!selector) {
                fn(evnt);
            }
        };
        inst.addEventListener(eventName, handler);
        dom.eventHandlers.push([fn, handler]);
        return detach = function () {
            const pair = dom.eventHandlers.find(([, h]) => handler === h);
            const index = dom.eventHandlers.indexOf(pair);
            if (index >= 0) {
                inst.removeEventListener(eventName, handler);
            } else {
                throw new Error(`Error in detach result. 
                      Can't detach unexisting ${eventName} handler`);
            }
        };
    },
    detach(inst, eventName, fn) {
        const pair = dom.eventHandlers.find(([key]) => fn === key) || [];
        const [, handler] = pair;
        const index = dom.eventHandlers.indexOf(pair);
        if (index >= 0) {
            dom.eventHandlers.splice(index, 1);
            inst.removeEventListener(eventName, handler);
        } else {
            setTimeout(() => {
                throw new Error(`Error in detach method. 
                      Can't detach unexisting ${eventName} handler`);
            });
        }
    },
    el(type, attrs, ...children) {
        attrs = attrs || {};
        children = [].concat(...children);
        const el = document.createElement(type);
        const attrNames = Object.keys(attrs);
        attrNames.forEach(attrName => {
            if (attrName in EVENT_NAMES) {
                const eventName = EVENT_NAMES[attrName];
                dom.attach(el, '', eventName, attrs[attrName]);
            } else if (attrName in el) {
                el[attrName] = attrs[attrName];
            } else {
                el.setAttribute(attrName, attrs[attrName]);
            }
        });
        for (const child of [...children]) {
            el.append(child);
        }

        return el;
    }
};

Strange feeling from the first look. All that is really needed is just attach/detach events and create elements. Despite that small list of tools. All that is here is really important. Events will be attached and detached very frequently. In the worst case with every state update. Sounds pretty creepy. It can be optimized. The optimization part would depend on the implementation.

The second sub layer is more interesting. It would contain more operations on the DOM. Those operations are related to comparing changes on the Virtual DOM. Perform update UI when needed. The UI is going to be updated when the state update has something to show.

The virtual DOM - second part of the DOM manipulations.

JavaScript
import { dom } from './dom';
import { arrayMerge } from './utils'; 


export const EVENT_NAMES = {
    onClick: 'click',
    onInput: 'input',
    onKeyPress: 'keypress',
    onChange: 'change',
    onSubmit: 'submit',
    onKeyDown: 'keydown',
    onKeyUp: 'keyup',
    onBlur: 'blur',
    onMouseDown: 'mousedown'
};

export const propConverters = {
    contentEditable: function (value) {
        return !!value;
    },
    convert(propName, value) {
        if (propName in propConverters) {
            return propConverters[propName](value);
        }
        return value;
    }
};

export function el(type, attrs = {}, ...children) {
    children = [].concat(...children)
        .filter(a => [undefined, true, false].indexOf(a) === -1)
        .map(item => (['object', 'function'].indexOf(typeof item) === -1 ? '' + item : item));
    if (typeof type === 'function') {
        return type({ ...attrs, store: currentStore, 
        children: children.length > 1 ? children : children[0] }, children);
    }

    return {
        type,
        attrs,
        children
    };
}

export let currentStore = null;

export function makeVdom(oldDom, store) {
    currentStore = store;
    function createElement(node) {
        if (node === undefined) {
            return document.createTextNode('');
        }
        if (['object', 'function'].indexOf(typeof node) === -1) {
            return document.createTextNode(node);
        }
        if (typeof node.type === 'function') {
            const { children = [] } = node;
            const [first] = children;
            const $el = createElement(first);
            patch($el, node.type, { ...node.attrs, store });
            return $el;
        }
        const { type, attrs = {}, children } = node;
        const { ref, ...attributes } = (attrs || {});
        const el = dom.el(type, attributes, 
                   ...[].concat(children).map(child => createElement(child)));
        return el;
    }

    function compare($el, newNode, oldNode) {
        if (typeof newNode !== typeof oldNode) {
            return true;
        }
        if (['object', 'function'].indexOf(typeof newNode) === -1) {
            if (newNode !== oldNode) {
                const oldValue = $el.textContent;
                if (oldValue !== newNode) {
                    return true;
                }
            }
            return false;
        }
        return newNode.type !== oldNode.type
    }

    function updateAttribute($el, newValue, oldValue, key) {
        if (oldValue === undefined) {
            $el.setAttribute(key, newValue);
        } else if (newValue === undefined) {
            $el.removeAttribute(key);
        } else if (newValue !== oldValue) {
            $el.setAttribute(key, newValue);
        }
    }

    function updateProperty($el, newValue, oldValue, key) {
        const oldElValue = $el[key];
        if (oldValue === undefined) {
            if (oldElValue !== newValue) {
                $el[key] = propConverters.convert(key, newValue);
            }
        } else if (newValue === undefined) {
            if (oldElValue !== newValue) {
                $el[key] = propConverters.convert(key, newValue);
            }
        } else if (newValue !== oldValue) {
            if (oldElValue !== newValue) {
                $el[key] = propConverters.convert(key, newValue);
            }
        }
    }

    function updateEvent($el, newHandler, oldHandler, key) {
        const eventName = EVENT_NAMES[key];
        if (!oldHandler) {
            dom.attach($el, '', eventName, newHandler);
        } else if (!newHandler) {
            dom.detach($el, eventName, oldHandler);
        } else {
            dom.detach($el, eventName, oldHandler);
            dom.attach($el, '', eventName, newHandler);
        }
    }

    function updateAttributes($el, newAttrs, oldAttrs) {
        newAttrs = newAttrs || {};
        oldAttrs = oldAttrs || {}
        const newKeys = Object.keys(newAttrs);
        const oldKeys = Object.keys(oldAttrs);
        const allKeys = arrayMerge(newKeys, oldKeys);
        for (const key of allKeys) {
            if (key in EVENT_NAMES) {
                updateEvent($el, newAttrs[key], oldAttrs[key], key);
            } else if (key in $el) {
                updateProperty($el, newAttrs[key], oldAttrs[key], key);
            } else {
                updateAttribute($el, newAttrs[key], oldAttrs[key], key);
            }
        }
    }

    function detachEvents($el, oldAttrs) {
        oldAttrs = oldAttrs || {}
        const oldKeys = Object.keys(oldAttrs);
        for (const key of oldKeys) {
            if (key in EVENT_NAMES) {
                updateEvent($el, undefined, oldAttrs[key], key);
            }
        }
    }

    function updateElement($parent, newNode, oldNode, index = 0) {
        let nodesToRemove = [];
        if (!oldNode) {
            $parent.appendChild(
                createElement(newNode)
            );
        } else if (!newNode) {
            detachEvents($parent.childNodes[index], oldNode.attrs);
            nodesToRemove.push($parent.childNodes[index]);
        } else if (compare($parent.childNodes[index], newNode, oldNode)) {
            detachEvents($parent.childNodes[index], oldNode.attrs);
            $parent.replaceChild(
                createElement(newNode),
                $parent.childNodes[index]
            );
        } else if (newNode.type) {
            updateAttributes($parent.childNodes[index], newNode.attrs, oldNode.attrs);
            const length = Math.max(newNode.children.length, oldNode.children.length);
            for (let i = 0; i < length; i++) {
                nodesToRemove = [
                    ...nodesToRemove,
                    ...updateElement(
                        $parent.childNodes[index],
                        newNode.children[i],
                        oldNode.children[i],
                        i
                    )];
            }
        }
        if (newNode && newNode.attrs && newNode.attrs.ref) {
            newNode.attrs.ref($parent.childNodes[index]);
        }
        return nodesToRemove;
    }

    function patch($el, view, props = {}) {
        const newDom = view({
            ...props,
            store,
            render(props) { patch($el, view, props); }
        });
        const removedNodes = updateElement($el, newDom, oldDom);
        removedNodes.map(node => node.parentElement.removeChild(node));
        oldDom = newDom;
    }

    return patch;
}

Here, I would omit the part that describes the main approach on how to find differences and apply changes. It is described in this article: How to write your own Virtual DOM. Here, I will focus on new parts that are related to the integration.

The main function here is makeVdom. This is the initialization method. It creates an initial Virtual DOM state. It takes two parameters: oldDom and state. OldDom is null by default. It will keep the previous version of the Virtual DOM. Used to compare changes and produce updates on UI. state is to provide the global state of the application into every component. The global state will be injected into every render component method. makeVdom function returns patch function. The patch function is going to be called very frequently. On render initial UI, refresh UI when state is updated, render/re-render child components. The el function will be used in JSX/TSX templates.

Global Predictable State

If in OOP state of the application is highly parted. As a member of the classes. In the functional paradigm, the state is less partitioned. Ideally, it is just one big hierarchical data structure. Keeps all state in a single place. Good part, the global state is easy to manage. Reads can be performed in parallel. The trickiest part is when it comes to updating the global state. Sequential updates are ideal to modify global state. When several functions are to update the state. Only one function performs an update. The next coming functions are queued, waiting for the future execution.

The Store

JavaScript
export function createStore(reducer, initialState, enhancer = null) {
    const currentReducer = reducer;
    let currentState = initialState;
    const listeners = [];

    if (enhancer) {
        return enhancer(createStore)(
            reducer,
            initialState
        );
    }

    return {
        getState() {
            return currentState;
        },
        dispatch(action) {
            currentState = currentReducer(currentState, action);
            listeners.map(listener => listener(currentState));
            return action;
        },
        subscribe(newListener) {
            listeners.push(newListener);
            return function () {
                const index = listeners.indexOf(newListener);
                if (index >= 0) {
                    listeners.splice(index, 1);
                }
            };
        }
    };
}

A getState method - provides a way to request global state. A dispatch method - provides a queue for sequential state updates. subscribe - method to listen for updates on store and refresh view. More information about creating a store is described in this article: Learn Redux by coding a Mini-Redux.

Please, note. It comes from a similar story about what was done with Virtual DOM. Now it is for the state. The second part of the store is the composition of creating state functions. Here are the rules how the final state of the store can be organised. More on that is well described in the article: Code your own Redux part 2: the connect function.

JavaScript
import { createStore } from './createStore';
import { reducer } from './reducers';
import { rootEffect } from './components';


export default function compose(...funcs: Function[]) {
    if (funcs.length === 0) {
      // infer the argument type so it is usable in inference down the line
      return <t>(arg: T) => arg
    }
  
    if (funcs.length === 1) {
      return funcs[0]
    }
  
    return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}

// Shamelessly stolen from this place: 
// https://github.com/reduxjs/redux/blob/master/src/applyMiddleware.ts
// and slightly adjusted
export function applyMiddleware(...middlewares) {
    return function (createStoreFn: typeof createStore) {
        return function (reducer, initialState) {
            const store = createStoreFn(reducer, initialState);
            let dispatch = (action, ...args) => { throw Error
                (`Dispatching {${JSON.stringify(action)}} while creating middleware`) };
            const middlewareAPI = {
                getState: store.getState,
                dispatch: (action, ...args) => dispatch(action, ...args)
            }
            const chain = middlewares.map(middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);

            return {
                ...store,
                dispatch
            };
        };
    }
}

function effectsMiddleware(store) {

    setTimeout(() => {
        store.dispatch({ type: '@INIT' });
    });

    let handler = rootEffect();

    return function (dispatch) {

        return function (action) {
            const ret = dispatch(action);

            for (const a of [].concat(...handler(action, store.getState(), 
                 (a) => store.dispatch(a)))) {
                store.dispatch(a);
            }

            return ret;
        };
    };
}

const createThunkMiddleware = (extraArgument?) => 
                       ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
    }

    return next(action);
};

var thunk = createThunkMiddleware();

export const store = createStore(reducer, { items: [], itemName: '' }, applyMiddleware(
    thunk,
    effectsMiddleware
));
</t>

This complex layer is responsible for the shape of the state. Custom processing of the state after update. When a store message is dispatched to the store. In OOP, it could be ingested per class. With the help of the Observer pattern. Eventually if to take a close look at the effectsMiddleware function. There is emerging another design pattern from the OOP - Publish Subscribe.

The other legacy of the functional paradigm is that there is nothing to make new. Every solution looks similar to each other. The whole application looks like a one composite function. It is like an emitting waves star. And the only way to measure it is to send input data and observe output.

The storage is the updating state system and it is a message dispatching hub. Every time a new message is dispatched to the store, it would update state according to the rules. Defined in reducers. Refresh views, defined in templates. Then the message, along with the updated state, will be passed for the extra processing to the rootEffect routine.

To get a better vision on the store and global state, let's try to find similarities between DOM elements, DOM events and DOM element properties. The DOM is declarative by nature. And Store is functional by nature. The main entry into the functional world. A store with Views listens to DOM updates. Transforms DOM events with properties into messages. Observe and synchronise relevant property values from the DOM elements with the global state.

The last block to complete the picture is the connect method.

JavaScript
export function connect(mapStateToProps, mapDispatchToProps) {
    let unsubscribe = () => { };
    return function (wrappedTemplate) {
        return function (p) {
            const { render, store = vdom.currentStore, ...props } = p;
            unsubscribe();
            unsubscribe = store.subscribe((state) => {
                render && render(props);
            });

            return wrappedTemplate({
                ...props,
                ...mapStateToProps(store.getState(), props),
                ...mapDispatchToProps(store.dispatch, props)
            });
        }
    }
}

This function "connects" both paradigms, declarative approach and functional. The store.subscribe method is for the declarative part. On subscribe the render method is called. The whole view will be re-rendered, updated according to the store state. The rootEffect is for the functional part. Do you see how it is important to be connected in our time? ;)

On this step, just for easy comprehension. The store can be treated as the functional DOM adapter. Every onclick, onchange, etc. events are messages. Every DOM element property is a part of the state. Similarly, we attach listeners to the DOM elements and read, for instance, value property. Same we can do now with the help of the rootEffect method. It is described at the end of the article. This time about the messages and state. Every element can have many events. And DOM eventually can have many elements. Now you can imagine the amount of kinds of messages that could be produced by this setup. We can think in the same way for the DOM element properties. This is the perfect input system. It allows to control any part of the UI at any moment in any way. The central commands dispatching queue with sequential updates. Similar to the command line. The functional universe was just born. Instead of display, it can listen and talk.

Action Messages and State Snippet Reducers

Till now, the application is mute. Let's break the silence. The form in which applications can update the state and dispatch events is well described in this article: Three ways to reduce the Redux boilerplate (and become more productive). The simple idea is to treat state updating tasks as simple update operations. Then all reducers can be separated per low level tasks. Sort of like how we program work with DataBase tables. Insert, update, delete operations.

In regards to this idea, it was introduced in the declareActions function.

JavaScript
export function createReducer (initialState) {
   return (reducerMap) => (state = initialState, action) => {
        const reducer = reducerMap[action.type];
        return reducer ? reducer(state, action) : state;
    }
};

type UnionToIntersection<U> = (U extends any
    ? (k: U) => void
    : any) extends ((k: infer I) => void)
    ? I
    : any;

type ActionArg<Y, T> = T extends (a: keyof Y, b: infer I, c?) => any ? I : any;

export function declareActions<T extends {
    [type in keyof T]: {
        [name in keyof T[K]]: (type: type, props: P, meta?) => any;
    };
}, K extends keyof T, KK extends keyof UnionToIntersection<T[K]>, 
   P, O extends UnionToIntersection<T[K]>>(
    actions: T
): [{ [key in KK]: (args?: ActionArg<T, O[KK]>, meta?) => any }, { [key in K]: K }, any] {
    const reducers = {};
    const keys = Object.keys(actions);
    const resActions = keys.reduce((res, type) => {
        const actionDecl = actions[type];
        let left = res[0],
            right = res[1];
        const actionNames = Object.keys(actionDecl);
        const reducer = actionDecl.reducer;
        if (reducer) {
            reducers[type] = reducer;
        }
        const actionFn = actionDecl[actionNames[0]];
        left = { ...left, [actionNames[0]]: (props, meta) => actionFn(type, props, meta) };
        right = { ...right, [type]: type };

        return [left, right];
    }, [{}, {}]);
    return [...resActions, createReducer({})(reducers)] as any;
}

export const selectPayload = ({ payload }) => payload;

It is a building method. For simple update state commands. It allows to build message processing blocks (reducers). Along with messages (actions) and message types. The tradeoff is that it dictates the following format for the message.

The shape of the action message.

JavaScript
{
    type: "UI_CREATE_TODO",
    payload: {
        title: "New todo item title"
    }
}

Here below are listed all constructions that are responsible for the shape of the global state.

This is how it is going to be updated for the todo list.

JavaScript
export const [MainActions, MainActionTypes, mainReducer] = declareActions({
    UI_CREATE_TODO: {
        uiCreateTodo: (type, payload) => ({ type, payload })
    },
    UI_UPDATE_NEW_TITLE: {
        uiUpdateNewTodoTitle: (type, payload) => ({ type, payload })
    },
    UI_TOGGLE_ALL_COMPLETE: {
        uiToggleAllComplete: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                toggleAllComplete: payload
            };
        }
    },
    UI_SET_ACTIVE_FILTER: {
        uiSetActiveFilter: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                activeFilter: payload
            };
        }
    },
    UI_CLEAR_COMPLETED: {
        uiClearCompleted: (type, payload) => ({ type, payload })
    },
    UPDATE_MAIN_NEW_TODO_TITLE: {
        updateNewTodoTitle: (type, payload: string) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                newTodoTitle: payload
            };
        }
    },
    UPDATE_MAIN_ITEMS: {
        updateItems: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                items: payload,
                activeItems: selectActiveItems(payload),
                completeItems: selectCompleteItems(payload)
            }
        }
    }
});

In regards to naming of the messages, there are messages that come from a UI. Those messages are named with the prefix UI_. Others, that could cause UI updates. For updating a global state. Triggering async operations don't have any prefix. This small convention would help to focus on UI related stuff and the rest.

This is how the one list item is going to be updated.

JavaScript
export const [ItemActions, ItemActionTypes, itemReducer] = declareActions({
    UI_UPDATE_CURRENT_TITLE: {
        uiUpdateCurrentTitle: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                title: payload
            };
        }
    },
    UI_SET_CURRENT_ITEM: {
        uiSetCurrentItem: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                id: payload
            };
        }
    },
    UI_UPDATE_TODO_TITLE: {
        uiUpdateTodoTitle: (type, payload) => ({ type, payload })
    },
    UI_SET_COMPLETE: {
        uiSetComplete: (type, id, complete) => ({ type, payload: { id, complete } })
    },
    UI_REMOVE_ITEM: {
        uiRemoveItem: (type, payload) => ({ type, payload })
    },
    SET_CURRENT: {
        setCurrent: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { payload }) => {
            return {
                ...state,
                ...payload
            };
        }
    }
});

The other advantage is that such declarations help to start writing actions, action types and reducers. Keep them at one place. For the fast checking what would happen with the global state when the message is dispatched.

The next set of messages and reducers. This time, it is to update state. The data comes from backend requests.

JavaScript
export const [ToDoActions, ToDoActionTypes, toDoReducer] = declareActions({
    FETCH_TODOS_ERROR: {
        fetchTodosError: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                error: payload
            };
        }
    },
    FETCH_TODOS_RESULT: {
        fetchTodosResult: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                loading: false,
                items: selectItemsById(payload),
                order: selectItemsOrder(payload)
            };
        }
    },
    FETCH_TODOS: {
        fetchItems: (type, payload) => dispatch => {
            const adapter = new TodosAdapter();
            (async () => {
                try {
                    const items = await adapter.fetchTodos();
                    dispatch(ToDoActions.fetchTodosResult(items));
                } catch (ex) {
                    dispatch(ToDoActions.fetchTodosError(ex));
                }
            })();
            return {
                type,
                payload: true
            }
        },
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                loading: payload
            };
        }
    },
    CREATE_TODO_ERROR: {
        createTodoError: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                error: payload
            };
        }
    },
    CREATE_TODO_RESULT: {
        createTodoResult: (type, payload) => ({ type, payload }),
        reducer: ({ loading, ...state}: any = {}, { type, payload }) => {
            return {
                ...state,
                items: {
                    ...selectItems(state),
                    [payload.id]: payload
                },
                order: [payload.id, ...selectOrder(state)]
            };
        }
    },
    CREATE_TODO: {
        createTodo: (type, title) => dispatch => {
            const adapter = new TodosAdapter();
            (async () => {
                try {
                    const item = await adapter.createTodo(title);
                    dispatch(ToDoActions.createTodoResult(item));
                } catch (ex) {
                    dispatch(ToDoActions.createTodoError(ex));
                }
            })();
            return {
                type,
                payload: true
            }
        },
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                loading: payload
            };
        }
    },
    UPDATE_TODO_ERROR: {
        updateTodoError: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                error: payload
            };
        }
    },
    UPDATE_TODO_RESULT: {
        updateTodoResult: (type, payload) => ({ type, payload }),
        reducer: ({ loading, ...state}: any = {}, { type, payload }) => {
            return {
                ...state,
                items: {
                    ...selectItems(state),
                    [payload.id]: payload
                }
            };
        }
    },
    UPDATE_TODO: {
        updateTodo: (type, id, attrs) => dispatch => {
            const adapter = new TodosAdapter();
            (async () => {
                try {
                    const item = await adapter.updateTodo(id, attrs);
                    dispatch(ToDoActions.updateTodoResult(item));
                } catch (ex) {
                    dispatch(ToDoActions.updateTodoError(ex));
                }
            })();
            return {
                type,
                payload: true
            }
        },
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                loading: payload
            };
        }
    },
    DELETE_TODO_ERROR: {
        deleteTodoError: (type, payload) => ({ type, payload }),
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                error: payload
            };
        }
    },
    DELETE_TODO_RESULT: {
        deleteTodoResult: (type, payload) => ({ type, payload }),
        reducer: ({ loading, ...state }: any = {}, { type, payload: id }) => {
            const { [id]: removed, ...items } = selectItemsInternal(state) as any;
            return {
                ...state,
                items,
                order: selectOrder(state).filter(pos => pos !== id)
            };
        }
    },
    DELETE_TODO: {
        deleteTodo: (type, id) => dispatch => {
            const adapter = new TodosAdapter();
            (async () => {
                try {
                    const item = await adapter.deleteTodo(id);
                    dispatch(ToDoActions.deleteTodoResult(id));
                } catch (ex) {
                    dispatch(ToDoActions.deleteTodoError(ex));
                }
            })();
            return {
                type,
                payload: true
            }
        },
        reducer: (state: {} = {}, { type, payload }) => {
            return {
                ...state,
                loading: payload
            };
        }
    }
});

Those messages are for backend communication. They can be divided into commands: create, update, delete. And divided by request. Messages that start a remote request. Messages that come with results from a backend. Messages that are for error reporting.

Everything is combined into this final reducer.

import { mainReducer, selectMain } from './components/main';
import { toDoReducer, selectTodos } from './models/todos';
import { itemReducer, selectCurrent } from './components/todoItem';


export const reducer = (prevState, action) => {
    const state = {
        ...prevState,
        main: mainReducer(selectMain(prevState), action),
        todos: toDoReducer(selectTodos(prevState), action),
        current: itemReducer(selectCurrent(prevState), action)
    };
    const { type, payload } = action;
    switch (type) {
        default:
            return state;
    }
};

In OOP, such kind of information usually was organised in the class methods and class members. This is similar. All can be grouped by separate files with the help of the declareActions method. It is always reminding me that I am just one step to OOP. Maybe this is because I can't switch enough from OOP to functional paradigm. CREATE_TODO, UPDATE_TODO and DELETE_TODO - actually should be smaller. For the sake of brevity. (async () => { ... }) was inlined within the message.

Views

The shape of the global state is adjusted. There are rules on how to update the state. Now this is the part to read the state. Views are the first participants that would actively read the state. Select a part of the state to display.

Templates

Templates are rendered with the help of Virtual DOM. And since it is VDOM, we can fully leverage JSX/TSX templating technology. It would be handy to produce The Virtual DOM state. Render UI. More information about JSX can be found over the internet. Here, I just list the templates that will be used for the solution. In this article, the word "component" is used for the view that is connected to the global state. Over the connect function. And the word "control" is used for the simple views that are sufficient by themselves. Not connected to the global state.

This is the template for the main component.

JavaScript
/** @jsx el */
import { el } from '../../virtualDom';
import { connect } from '../../connect';
import { className as cn } from '../../utils';
import { TodoListView } from '../../controls/todoListView';
import { TodoListViewItem } from '../todoItem';
import { selectMain } from '.';
import { Actions } from '../';
import { bindActions } from '../../bindActions';

const ENTER_KEY = 13;

function mapStateToProps(state, props: {
    error;
    showClearCompleted;
    activeFilter;
    hasTodos;
    items;
    completeItems;
    activeItems;
    totalText;
    todoCount;
    manyTasks;
}) {
    const newState = {
        ...props,
        ...selectMain(state)
    };
    return {
        ...newState,
        errors: {
            main: newState.error,
            todos: state.todos && state.todos.error
        }
    };
}

function mapDispatchToProps(dispatch, props) {
    const actions = bindActions(Actions, dispatch);
    return {
        dispatch: dispatch,
        onKeypress(evnt) {
            if (evnt.which === ENTER_KEY) {
                actions.uiCreateTodo();
            }
        },
        ...actions
    };
}

export const MainView = connect(mapStateToProps, mapDispatchToProps)
                        (({ dispatch, ...props } = {

} as ReturnType<typeof mapDispatchToProps> & ReturnType<typeof mapStateToProps>) => <main>
    <section className="todoapp device-content">
        <header className="bar bar-nav">
            <button className={cn('btn pull-left ?active', props.toggleAllComplete)}>
                <input className="toggle-all hidden" id="toggle-all" type="checkbox"
                    defaultChecked={!!props.toggleAllComplete}
                    onClick={e => props.uiToggleAllComplete(e.target['checked'])}
                />
                <label htmlFor="toggle-all">Complete All</label>
            </button>
            <button className={cn('clear-completed btn pull-right ?hidden', 
                               props.showClearCompleted)}
                onClick={e => props.uiClearCompleted()}
                >Clear completed</button>
            <hr/>
            <div className="filters segmented-control">
                <a className={cn("control-item ?active", !props.activeFilter)} href="#/"
                    onClick={evnt => props.uiSetActiveFilter('all')}
                    >All</a>
                     • 
                <a className={cn("control-item ?active", 
                 props.activeFilter === 'active')} href="#/active"
                    onClick={evnt => props.uiSetActiveFilter('active')}
                    >Active</a>
                     • 
                <a className={cn("control-item ?active", 
                 props.activeFilter === 'completed')} href="#/completed"
                    onClick={evnt => props.uiSetActiveFilter('complete')}    
                    >Completed</a>
            </div>
        </header>
        <hr/>
        <section className="bar bar-standard bar-header-secondary">
            <form onSubmit={e => e.preventDefault()}>
                <input className="new-todo" 
                 type="search" placeholder="What needs to be done?"
                    value={props.newTodoTitle}
                    onInput={e => props.uiUpdateNewTodoTitle(e.target['value'])}
                    onKeyPress={e => props.onKeypress(e)}
                />
            </form>
        </section>
        <hr/>
        <footer className={cn('footer bar bar-standard bar-footer ?hidden', props.hasTodos)}>
            <span className="todo-count title">
                <strong>{props.activeItems ? props.activeItems.length : 0}</strong> 
            {!(props.activeItems && props.activeItems.length === 1)
                    ? <span className="items-word">items</span>
                    : <span className="item-word">item</span>
                }
             left from 
            <span className="total">{props.items ? props.items.length : 0}</span>
            </span>
        </footer>
        <section className={cn('content ?hidden', props.hasTodos)}>
                <TodoListView items={
                    props.activeFilter === 'complete'
                        ? props.completeItems
                        : props.activeFilter === 'active'
                            ? props.activeItems
                            : props.items
                }>
                {item => TodoListViewItem(item)}
            </TodoListView>
            <footer className="info content-padded">
                <p>Click to edit a todo</p>
            </footer>
        </section>
    </section>
</main>);

The template for the list item component.

JavaScript
/** @jsx el */
import { el } from '../../virtualDom';
import { connect } from '../../connect';
import { className as cn } from '../../utils';
import { selectCurrent, selectTitle } from './';
import { Actions } from '../';
import { bindActions } from '../../bindActions';


const ENTER_KEY = 13;
export const ESC_KEY = 27;

function mapStateToProps(state, props) {
    const newState = {
        ...props,
        current: selectCurrent(state)
    };
    return {
        ...newState,
        errors: {
            main: newState.error,
            todos: state.todos && state.todos.error
        }
    };
}

function mapDispatchToProps(dispatch, props) {
    const actions = bindActions(Actions, dispatch);
    return {
        dispatch: dispatch,
        ...actions,
        updateOnEnter(evnt, itemId) {
            if (evnt.which === ENTER_KEY) {
                actions.uiUpdateTodoTitle(itemId);
                actions.uiSetCurrentItem(null);
            }
        },
        revertOnEscape(e) {
            if (e.which === ESC_KEY) {
                actions.uiSetCurrentItem(null);
                //this.cancelChanges();
            }
        }
    };
}

// this part should be extracted into the separate file
// it has kept here just to make easy understand the implementation
function setCaretAtStartEnd(node, atEnd) {
    const sel = document.getSelection();
    node = node.firstChild;
  
    if (sel.rangeCount) {
        ['Start', 'End'].forEach(pos =>
            sel.getRangeAt(0)["set" + pos](node, atEnd ? node.length : 0)
        )
    }
}
// this part should be extracted into the separate file
// it has kept here just to make easy understand the implementation
function focusEditbox(el) {
    el.focus();
    if (el.textContent) {
        setCaretAtStartEnd(el, true);
    }
}

export const TodoListViewItem = connect(mapStateToProps, mapDispatchToProps)
                                (({ dispatch, ...props } = {

} as ReturnType<typeof mapDispatchToProps> & 
     ReturnType<typeof mapStateToProps>) => <div className={cn(
    'table-view-cell', 'media', 'completed?', 'editing?', 'hidden?',
    props.completed, props.current.id, props.hidden
)}>
        <span className="media-object pull-left" style={"display: inline-block;" as any}>
            <input id={`view-${props.id}`} className="hidden" type="checkbox"
                checked={props.complete}
                onChange={e => props.uiSetComplete(props.id, e.target['checked'])}
            />
        </span>
        <span className="input-group" style={"display: inline-block; width: 70%;" as any}>
            {(props.current && props.current.id === props.id) || 
             <label className="view input" style={"padding: 1px 1px 1px 1px;" as any}
                onClick={e => props.uiSetCurrentItem(props.id)}
            >{props.title}</label>}
            {(props.current && props.current.id === props.id) && 
            <div className="edit" style={"border: 1px solid grey;outline: none;" as any}
                contentEditable={true}
                ref={el => focusEditbox(el)}
                onInput={e => props.uiUpdateCurrentTitle(e.target['innerText'])}
                onKeyPress={e => props.updateOnEnter(e, props.current.id)}
                onKeyUp={e => props.revertOnEscape(e)}
                onBlur={e =>props.uiSetCurrentItem(null)}
            >{props.current.title}</div>}
        </span>
        <button className="destroy btn icon icon-trash"
            onClick={e => props.uiRemoveItem(props.id)}
            style={"display: inline-block;" as any}
        >Delete</button>
    </div>
);

And the template for the list control.

JavaScript
/** @jsx el */
import { el } from '../virtualDom';

const map = (items, fn) => {
    const res = [];
    for (let key in items) {
        if (items.hasOwnProperty(key)) {
            res.push(fn(items[key], key));
        }
    }
    return res;
}

export const TodoListView = ({ items = [], children = item => '' + item }: 
          { items: [] | {}, children?}) => <ul style={"padding: 0px;" as any}>
    {map(items, (item, index) => <li style={"list-style: none;" as any}>
                                 {children(item, index)}</li>)}
</ul>;

The last is the small one. It is a template for the list view. Similar to MVVM. The same approach is used to build controls without state. They are used as simple controls to do simple tasks. For this case, it is to list todo items.

Besides the JSX markup, the important parts in a template are those methods: mapStateToProps and mapDispatchToProps. Since it is Virtual DOM. It will be frequently re-rendered. Everything that is in mapStateToProps and mapDispatchToProps should be written with caution. This is the first place that could cause performance degradation of the whole application. Even if nothing is going to be updated in a UI, it would be re-rendered with every dispatch operation. Selectors that are used here should be quick.

Selectors should provide the part of the state just enough for the view. This is important. When an application grows. It would be useful to quickly find what part of the data is used and in which view. Just by observing the top of the template. The next important part here is function bindActions. The function is used to bind every action to the dispatch function from the state. Then it can be used to trigger relevant action.

This is how the bindActions function is defined.

JavaScript
export function bindActions<A, K extends keyof A>(actions: A, dispatch) {
    const res = Object.keys(actions).reduce((res, key) => ({
        ...res,
        [key]: (...args) => dispatch(actions[key](...args))
    }), {} as { [key in K]: A[K] });

    return res;
}

And this is how all actions in the application are merged into the Actions object.

JavaScript
import { MainActions } from './main';
import { ItemActions } from './todoItem';


export const Actions = {
    ...MainActions,
    ...ItemActions
};

Next - are selectors.

Selectors

The idea of selectors is already explained in many articles. I would just mention that selectors are to slice the global state into a small part. Selectors can be combined in regards to how they are slicing data. There are selectors that tend to dive deep through the global state and produce sub state snippets. And selectors that would work in the composition. Pick a part of the necessary data as an input for other selectors. A part of the data snippet will be provided as an argument for other selectors that build the final sub snippet.

Selectors are not only a part of the views. They will be used in many places.

Here is the list of the selectors from the backend module.

JavaScript
const selectItemsById = (items = []) => items.reduce((res, item) => ({
    ...res,
    [item.id]: item
}), {});
const selectItemsOrder = (items = []) => items.map(({ id }) => id);
const selectOrder = ({ order }) => order;
export const selectTodos = ({ todos }) => todos;
export const selectItemsInternal = ({ items = {} }) => items;
export const selectItems = ({ items = {}, order = [] }) => order.map(id => items[id]);

The list of the selectors for the main module.

JavaScript
export const selectMain = ({ main = { newTodoTitle: '', toggleAllComplete: false } }) => main;
export const selectItems = ({ items = [] }) => items;
export const selectItemIsComplete = ({ complete }) => complete;
export const selectActiveItems = 
    (items = []) => pick(items, item => !selectItemIsComplete(item));
export const selectCompleteItems = 
    (items = []) => pick(items, item => selectItemIsComplete(item));
export const selectNewTodoTitle = ({ newTodoTitle = '' }) => newTodoTitle;
export const selectToggleAllComplete = ({ toggleAllComplete }) => toggleAllComplete;

The list of the selectors for the list item module

JavaScript
export const selectCurrent = ({ current = {} }) => current;
export const selectTitle = ({ title }) => title;
export const selectId = ({ id }) => id;
export const selectComplete = ({ complete }) => complete;

To summarise, the selectors are similar to class getters. Selectors are evaluated frequently. Should be quick. Ideally, to pick up a small part of the state. And reducers are similar to class setters. Setters should be small. Ideally are for the one task: create, update, delete.

Effects

The answer to the databinding in declarative development. This time from the functional paradigm.

Like we can attach an event to the DOM document element. And be overwhelmed with the amount of events that are triggered when the cursor is moved. Mouse clicked. Key on the keyboard is pressed. UI button is clicked. The same picture is with the rootEffect function. Every element on the View can trigger a message. When the application is small. It would produce a small amount of messages. With a time, when views will become advanced. More actions will be attached to element events. The amount of messages will grow. Every message will be delivered to the store. State will be updated. Then, the same message with the new state will be passed as the parameter to the rootEffect method. Now the message will live another life.

Let's take a closer look to the effect for the list item component.

JavaScript
export const currentItem = () => {

    const fromView = merge(
        pipe(
            whenSetCurrentItem,
            withArg(pipe(onState, queryTodos, queryItems)),
            map(([{ payload: id }, items]: [any, any[]]) => {
                const currentItem = items.find(item => item.id === id);
                return ItemActions.setCurrent(currentItem);
            })
        ),
        pipe(
            whenUpdateTodoTitle,
            withArg(pipe(onState, queryCurrent, queryId), 
                    pipe(onState, queryCurrent, queryTitle)),
            map(([a, itemId, editTitle]) => 
                  ToDoActions.updateTodo(itemId, { title: editTitle }))
        ),
        pipe(
            whenSetComplete,
            map(({ payload: { id, complete } }) => ToDoActions.updateTodo(id, { complete }))
        ),
        pipe(
            whenRemoveItem,
            map(({ payload: id }) => ToDoActions.deleteTodo(id))
        )
    );
    const fromService = merge(
        pipe(
            whenUpdateTodoResult,
            map(() => ToDoActions.fetchItems())
        )
    );

    return merge(
        fromView,
        fromService
    );
}

It is a composite function that would produce another function in the end. There is the catch. The whole currentItem method is declared as the function for a purpose. The function would build another function. The returning function would be a part of the rootEffect function.

Let's take a close look at how the rootEffect function is built.

JavaScript
import { main } from './main';
import { currentItem } from './todoItem';
import { merge } from '../itrx';


export const rootEffect = () => merge(
    main(),
    currentItem()
);

Once more. It is a function that builds another function. This is a common approach in functional programming. When the big task is split on small operations. There are functions for the smaller operations. And there is a composite function. It is a combination of the smaller functions. To perform a bigger task.

Here is the final handler function that would perform a big task. Part of the effectsMiddleware function.

JavaScript
...
// creates the function with can be defined like that
//  function handler(action, state, dispatch) { ... }
let handler = rootEffect();
...

And this is how it would be called:

JavaScript
...
// in reality used two arguments. actions and store.getState()
// (a) => store.dispatch(a)) is never used. It is left here for playground.
for (const a of [].concat(...handler(action, store.getState(), (a) => store.dispatch(a)))) {
    store.dispatch(a);
}
...

The result of the function is another set of actions. To be dispatched to the store. The view is programmed to dispatch actions. And the effects handler is programmed to dispatch actions. It is programmed in a special way.

Let's try to investigate the life cyrcle of the rootEffect function. It is created like that rootEffect(). And it is used like that rootEffect()(<action>, <state>). It is separated into two parts to improve performance. The first initialization faze executed only once. When the whole function is built. Then it will be executed very frequently. In this case, it would be called along with initialization. It would take a bigger road than it could.

The internal composition of the rootEffect function can be split on many small tasks. Here is another effect. This time for the main component.

JavaScript
export const main = () => {
    const init = merge(
        pipe(
            ofType('@INIT'),
            map(() => ToDoActions.fetchItems())
        )
    );
    const fromView = merge(
        pipe(
            whenCreateTodo,
            withArg(pipe(onState, queryMain, queryNewTodoTitle)),
            map(([a, newTodoTitle]) => ToDoActions.createTodo(newTodoTitle))
        ),
        pipe(
            whenUpdateNewTitle,
            map(({ payload }) => MainActions.updateNewTodoTitle(payload))
        ),
        pipe(
            whenToggleAllComplete,
            withArg(pipe(onState, queryTodos, queryItems), 
                    pipe(onState, queryMain, queryToggleAllComplete)),
            map(([, items, isCompleted]) => API.markAllItemsCompleted(items, isCompleted)),
            map(completedItems => completedItems.map
                                  (item => ToDoActions.updateTodo(item.id, item)))
        ),
        pipe(
            whenSetActiveFilter,
            withArg(pipe(onState, queryTodos, queryItems)),
            map(([a, todos]) => MainActions.updateItems(todos))
        ),
        pipe(
            whenClearCompleted,
            withArg(pipe(onState, queryTodos, queryItems, queryCompleteItems)),
            map(([a, completeItems = []]) => completeItems.map
                                             (item => ToDoActions.deleteTodo(item.id)))
        )
    );
    const fromService = merge(
        pipe(
            whenCreateTodoResult,
            map(() => MainActions.updateNewTodoTitle(''))
        ),
        pipe(
            merge(whenCreateItem, whenDeleteItem),
            map(() => ToDoActions.fetchItems())
        ),
        pipe(
            merge(whenChangeItems, whenCreateItem, whenDeleteItem),
            withArg(pipe(onState, queryTodos, queryItems)),
            map(([action, todos]) => {
                return MainActions.updateItems(todos);
            })
        )
    );

    return merge(
        init,
        fromView,
        fromService
    );
}

It works in this way. First, it would run the initial request. To fetch items from the backend. The action is ToDoActions.fetchItems(). Then it would listen for more messages from the UI. In the end, it would process internal commands. Such as reset todo title on a create new todo form. Run more backend requests. Refreshing todo items. Update UI with the fetched todo items from backend.

There are two types of custom operator functions. Sort of operator methods. Such as map, pipe, merge, ofType. As basic blocks to build a more advanced logic. And customized operator functions. Let's call them "query" and "when" functions. They will be used along with operator functions. They are to keep small logic composed into the custom operator function.

"query" functions are based on selectors and a map operator function. "when" functions are based on message type and ofType operator function.

This is the list of the all "query" and "when" custom operator functions. They are small building blocks to build effects.

JavaScript
// for the main module
const queryMain = map(selectMain);
const queryNewTodoTitle = map(selectNewTodoTitle);
const queryToggleAllComplete = map(selectToggleAllComplete);
const queryCompleteItems = map(selectCompleteItems);

const whenUpdateNewTitle = ofType(MainActionTypes.UI_UPDATE_NEW_TITLE);
const whenCreateTodo = ofType(MainActionTypes.UI_CREATE_TODO);
const whenToggleAllComplete = ofType(MainActionTypes.UI_TOGGLE_ALL_COMPLETE);
const whenSetActiveFilter = ofType(MainActionTypes.UI_SET_ACTIVE_FILTER);
const whenClearCompleted = ofType(MainActionTypes.UI_CLEAR_COMPLETED);

// for the todo item module
export const queryCurrent = map(selectCurrent);
export const queryTitle = map(selectTitle);
export const queryComplete = map(selectComplete);
export const queryId = map(selectId);

const whenSetCurrentItem = ofType(ItemActionTypes.UI_SET_CURRENT_ITEM);
const whenUpdateTodoTitle = ofType(ItemActionTypes.UI_UPDATE_TODO_TITLE);
const whenSetComplete = ofType(ItemActionTypes.UI_SET_COMPLETE);
const whenRemoveItem = ofType(ItemActionTypes.UI_REMOVE_ITEM);

// for the todo model
export const queryTodos = map(selectTodos);
export const queryItems = map(selectItems);

export const whenChangeItems = ofType(ToDoActionTypes.FETCH_TODOS_RESULT);
export const whenCreateItem = ofType(ToDoActionTypes.CREATE_TODO_RESULT);
export const whenDeleteItem = ofType(ToDoActionTypes.DELETE_TODO_RESULT);

export const whenCreateTodoResult = ofType(ToDoActionTypes.CREATE_TODO_RESULT);
export const whenUpdateTodoResult = ofType(ToDoActionTypes.UPDATE_TODO_RESULT);

Let's check the operator functions in detail.

JavaScript
export const onAction = (...abc) => [abc[0]];
export const onState = (...abc) => [abc[1]];
export const onDispatch = (...abc) => [abc[2]];

export const pipe = (...fn) => (...abc) => 
           fn.reduce((res, curr) => res.length ? curr(...res) : [], abc);
export const merge = (...fn) => (...abc) => 
           fn.reduce((res, curr) => [...res, ...curr(...abc)], []);

export const map = (fn) => (...abc) => [fn(...abc)];
export const withArg = (...fn) => (a, ...abc) => [[a, ...merge(...fn)(a, ...abc)], ...abc];
export const filter = (fn) => (...abc) => fn(...abc) ? [...abc] : [];

export const ofType = (type) => filter(a => a.type === type);

It is a small library, named "itrx". It provides blocks that perform small operations. Similar to streams. But this solution is for arrays.

Three functions onAction, onState and onDispatch. Those operator functions provide actions, state and dispatch arguments. They select just one argument passed to the handler (result of the rootEffects()) function.

The first method that is often used is pipe. This is a sort of pipeline method in functional programming. A bit customized. Since it is expected to dispatch messages. The result could be nothing, one message or many messages. For that case, all operator functions are adjusted to work with data as function or array. This is how it is defined.

Please, pay attention to the inline if. It would work till an array. And the array is not empty. Result could provide an empty array. Then the piping will stop processing the rest of the methods in arguments and return the result.

JavaScript
export const pipe = (...args) => 
    (...abc) => args.reduce((res, fn) => res.length ? fn(...res) : [], abc);

It would take the first method. Evaluate it and pass the result to the next method. Like this:

JavaScript
...
pipe(
    whenCreateTodo,
    withArg(pipe(onState, queryMain, queryNewTodoTitle)),
    map(([a, newTodoTitle]) => ToDoActions.createTodo(newTodoTitle))
),
...

First will be called method whenCreateTodo. Then the result will be passed to the method withArg. And finally, the next result will be passed to the map.

The merge function is designed to apply every function from ...fn passing same arguments ...abc. It is adjusted in the way to map every function provided in argument. Then merge the result into a single array.

This is how the merge function is used.

JavaScript
return merge(
    init,
    fromView,
    fromService
);

With introducing the merge function, it has become an ability to group messages from one component into a single unit.

The map method helps to pick an action that would be used in the end. Analog is the map method from array. Any custom logic can be used there. Try to keep it small. In the other case, it could produce big effects. And with a bigger effect, it is harder to understand what it does. Similar to templates.

JavaScript
map(({ payload }) => MainActions.updateNewTodoTitle(payload))

The most trickiest function is withArg. This is an arguments altering function for the next piped function. Let's take a closer look into it again.

JavaScript
export const withArg = (...fn) => (a, ...abc) => [[a, ...merge(...fn)(a, ...abc)], ...abc];

This is how it is used.

JavaScript
withArg(pipe(onState, queryTodos, queryItems)),
map(([action, todos]) => {
    return MainActions.updateItems(todos);
})

To get a better idea, let's check another map method. This time without altered arguments.

JavaScript
map(action => ToDoActions.updateTodo(action.payload.id, action.payload.complete))

Though, by default action, state and dispatch arguments will be passed into the map method. This is a sort of universal approach. Very often, it would require to build logic that is based on the part of the state. Though, we can use something like that: state.todo.items. Which would work for the small applications. In the future, referring deep into nested properties in the state could become a problem.

First, it is unsafe to request nested state over dots. Some parts could have a null value. That would lead to an error, e.g.,

VM135419:1 Uncaught TypeError: Cannot read property 'items' of null
    at <anonymous>:1:6
</anonymous>

Second. It produces a strong coupling to the state. Every evolution of the state structure would lead to update effects as well.

There is a solution to it. Selectors are to pick up a small portion of the state. Usually, when state structure is altered, selectors will be changed as well. We can reuse selectors one more. This time for building effects. What makes it slow is the convenient way to adjust selectors. Make selectors assist to build effects. And the withArg is the answer.

Let's check one more time.

JavaScript
withArg(pipe(onState, queryTodos, queryItems), 
        pipe(onState, queryMain, queryToggleAllComplete)),

Where the queryTodos and the queryItems query methods are piped. Once evaluated, they would extract a small portion of the state. Same is with the queryMain and the queryToggleAllComplete. Another part of the state. In the end, it would produce such a set of arguments as [items, isCompleted]. For the map operator function map(([, items, isCompleted]) => API.markAllItemsCompleted(items, isCompleted)). With this approach. It is unusual to admit that it would work. Once used, it becomes so handy that it is hard to ignore it when building effects.

The filter function will be used to pick the message by action type. And skip the rest. Similar to the filter method for array. With the help of the filter function, it becomes possible to build "when" operator functions, e.g.:

JavaScript
// assuming that
export const ofType = (type) => filter(a => a.type === type);

// then we can build such operator as
const whenCreateTodoResult = ofType(ToDoActionTypes.CREATE_TODO_RESULT);

From the first glance, it looks strange why so many small functions are just to call another function. When given a second though, it can be treated as the new language. Build on top of the programming language. In this article, it is typescript. For functional development. Another common approach. When function becomes the smallest simple building block. Gradually, it would emerge as another higher order language. Build on the syntax of the programming language. With a unique linguistic ecosystem. Similar to frameworks, that have emerged in a declarative world.

As for the effects, it can be treated as another way to declare data bindings. When binding commands are well defined and built as universal blocks. Everyone gets used to them. Knows how to use them. Similar case for the effects. This time with the option to build your own building commands. That would have a unique linguistic ecosystem. Just remember one rule. Effects module should be small. Consist of the small, easy understanding constructions. Similar like it is done with event listeners and event handlers.

Patient readers could notice that many parts are similar to React/Redux. However, it is omitted. The reason is that I try to focus on the Model View Update design pattern. React/Redux is one of the possible ways to implement it.

Conclusions About MVU Design Pattern

MVU design pattern is a functional approach.

Positives

Simple to understand. The smallest building block is a function.

The application state can be stored as the one global state.

Data flow is adjusted in one direction. From a view, over the actions and far to the state.

Updating state messages are organised in a sequential way. Less code to synchronise data when state is updating.

To every OOP approach, there is an answer from the functional world. Such as getters/setters and selectors/reducers. Databinding and effects.

Ability to produce unique high order language. Build on the top of the programming language.

Consequences

The tool is the only function. Leads to overuse of the high order functions.

The global state could grow. That could lead to logical fragmentation of the state.

The one direction of the data flow could lead to frequent extra updates with no changes on UI.

The state could become fragile. If wrong, updating a small part of the state, could break the whole UI.

There are already many tools for the OOP. Programming language syntax should support functional programming.

Leads to create high order language. Would require extra time to learn it.

Thank you for the reading. The code material in this article is based on the sources that can be found over the internet. My work was in putting it all together. Make it run as playground. Hope, it will be useful.

The List of Some of Them

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Volodymyr Kopytin
Software Developer at RebelMouse
Poland Poland
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --