4.2 Incoming / Outgoing Actions

Similar to RandomGif, we need to define Incoming / Outgoing Actions as external programming interfaces so that component users can interact with the component programmingly easily (in src/RandomGifPair/actions/types.js):

// --- Incoming Action. External user can send this action to trigger Gif Pair loading
export const REQUEST_NEW_PAIR = Symbol("REQUEST_NEW_PAIR");
// --- Out-going actions.
// --- Dispatch when loading starts
export const LOADING_START = Symbol("LOADING_START");
// --- Dispatch when loading complete
export const LOADING_COMPLETE = Symbol("LOADING_COMPLETE");

And, we can define the following action creator functions accordingly (in src/RandomGifPair/actions/index.js):

import * as actionTypes from "./types";

export function requestNewPair() {
    return {
        type: actionTypes.REQUEST_NEW_PAIR
    };
}

export function loadingStart() {
    return {
        type: actionTypes.LOADING_START
    };
}

export function loadingComplete(error = null) {
    return {
        type: actionTypes.LOADING_COMPLETE,
        payload: {
            isSuccess: error ? false : true,
            error
        }
    };
}

4.2.1 REQUEST_NEW_PAIR action

To support REQUEST_NEW_PAIR, we firstly need to modify the onClick handler of the Get Gif Pair button to the following:

<button
    onClick={() => {
        this.componentManager.dispatch(
            actions.requestNewPair()
        );
    }}
    disabled={this.state.isLoading}
>

Then, we need to create saga (in src/RandomGifPair/sagas/index.js) as the followings:

import * as actions from "../actions";
import * as actionTypes from "../actions/types";
import {
    actions as RandomGifActions,
    actionTypes as RandomGifActionTypes
} from "../../RandomGif";

function* mainSaga(effects) {
    // --- monitor `REQUEST_NEW_PAIR` and send multicast actions to RandomGifs
    yield effects.takeEvery(
        actionTypes.REQUEST_NEW_PAIR,
        function*() {
            yield effects.put(
                RandomGifActions.requestNewGif(),
                "./Gifs/*"
            );
        }
    );
}

export default mainSaga;

At last, modify src/RandomGifPair/index.js to register the saga accordingly (via ManageableComponentOptions / saga).

4.2.2 LOADING_START & LOADING_COMPLETE actions

To support, we need to find a way to capture the LOADING_START & LOADING_COMPLETE actions emitted from RandomGif component. From section 3.6.1.2, you can tell LOADING_START & LOADING_COMPLETE will be thrown just out of RandomGif Container (i.e. the first namespace part above RandomGif in the Namespace Tree). Thus, both actions will be dispatched to namespace path: ${this.componentManager.fullPath}/Gifs. Because multicast actions are always flow-down along the Namespace Tree, our RandomGifPair won't receive them.

To capture those actions, we can use a helper component: ActionForwarder. We can import it frpm fractal-component:

import { AppContainerUtils, AppContainer, ActionForwarder } from "fractal-component";

And add the followings to render() method (as ActionForwarder doesn't render anything, you can put it anywhere in React Component Tree):

{/**
    * Use ActionForwarder to forward LOADING_START & LOADING_COMPLETE actions from `RandomGif`
    * to current component (`RandomGifPair`)'s namespace.
    * i.e. from `${this.componentManager.fullPath}/Gifs` to `${this.componentManager.fullPath}`
    * Thus, `relativeDispatchPath` should be ".."
    */}
<ActionForwarder
    namespacePrefix={`${this.componentManager.fullPath}/Gifs`}
    pattern={action =>
        action.type === RandomGifActionTypes.LOADING_START ||
        action.type === RandomGifActionTypes.LOADING_COMPLETE
    }
    relativeDispatchPath=".."
/>

Now, our RandomGifPair component can receive both LOADING_START & LOADING_COMPLETE actions from RandomGif component (either in a reducer or saga). Before we move on to reducer or saga. We need to define the component initial state as followings:

this.state = {
    // --- record each `RandomGif` loading progress
    itemsLoading: {},
    // --- if `RandomGifPair` starts to load
    isLoading: false,
    // --- if any error received from any of the `RandomGif` component
    error: null
};

Next, we can create the reducer in src/RandomGifPair/reducers/index.js:

import { actionTypes as RandomGifActionTypes } from "../../RandomGif";

const reducer = function(state, action) {
    switch (action.type) {
        case RandomGifActionTypes.LOADING_START:
            return {
                ...state,
                isLoading: true,
                itemsLoading: {
                    ...state.itemsLoading,
                    [action.componentId]: true
                }
            };
        case RandomGifActionTypes.LOADING_COMPLETE: {
            const { isSuccess, payloadError } = action.payload;
            let { itemsLoading, error } = state;
            itemsLoading = {
                ...itemsLoading,
                [action.componentId]: false
            };
            let isLoading = false;
            Object.keys(itemsLoading).forEach(componentId => {
                if (itemsLoading[componentId]) isLoading = true;
            });
            return {
                ...state,
                isLoading,
                error: error ? error : isSuccess ? null : payloadError,
                itemsLoading
            };
        }
        default:
            return state;
    }
};
export default reducer;

At last, we can added the following to saga to dispatch RandomGifPair out-going actions:

function* mainSaga(effects) {
    let isLoadingStartActionDispatched = false;
    yield effects.takeEvery(
        RandomGifActionTypes.LOADING_START,
        function*() {
            // --- we use local variable `isLoadingStartActionDispatched`
            // --- to make sure only dispatch one `LOADING_START` action
            if (!isLoadingStartActionDispatched) {
                yield effects.put(
                    actions.loadingStart(),
                    "../../../*"
                );
            }
        }
    );

    yield effects.takeEvery(
        RandomGifActionTypes.LOADING_COMPLETE,
        function*() {
            /**
             * throw exposed action out of box
             * It's guaranteed all reducers are run before saga.
             * Therefore, if you get state in a saga via `select` effect,
             * it'll always be applied state.
             */
            const { isLoading, error } = yield effects.select();
            if (!isLoading) {
                yield effects.put(
                    actions.loadingComplete(error),
                    "../../../*"
                );
                isLoadingStartActionDispatched = false;
            }
        }
    );
}

4.2.3 NEW_GIF

We will also want to forward NEW_GIF from RandomGif components. Therefore, component users will know how many new gifs has been loaded in total by couting no. of NEW_GIF actions.

To do so, we need to add a new ActionForwarder to render() method:

{/**
    * Use ActionForwarder to throw NEW_GIF out of RandomGifPair container
    * Set namespace to `${this.componentManager.fullPath}/Gifs` in order to listen to
    * all `out of box` actions from two `RandomGif` components
    */}
<ActionForwarder
    namespacePrefix={`${this.componentManager.fullPath}/Gifs`}
    pattern={RandomGifActionTypes.NEW_GIF}
    relativeDispatchPath="../../../../*"
/>

4.2.4 Setup Out-Going / Incoming Actions

Firstly, we update the component registration code to register action types & set allowed incoming actions:

this.componentManager = new ComponentManager(this, {
    namespace: "io.github.t83714/RandomGif",
    reducer,
    saga,
    // --- register all action types so that actions are serialisable
    actionTypes,
    allowedIncomingMulticastActionTypes: [actionTypes.REQUEST_NEW_PAIR],
    namespaceInitCallback: componentManager => { ... },
    namespaceDestroyCallback: ({ styleSheet }) => { ... }
});

We also need to export the following actions from RandomGifPair entry point src/RandomGifPair/index.js:

//--- actions component may send out
const exposedActionTypes = {
    // --- export NEW_GIF action type as well just
    // --- so people can use `RandomGifPair` without knowing `RandomGif`
    NEW_GIF: RandomGifActionTypes.NEW_GIF,
    LOADING_START: actionTypes.LOADING_START,
    LOADING_COMPLETE: actionTypes.LOADING_COMPLETE,
    REQUEST_NEW_PAIR: actionTypes.REQUEST_NEW_PAIR
};
//--- action component will accept
const exposedActions = {
    requestNewPair: actions.requestNewPair
};

/**
 * expose actions for component users
 */
export { exposedActionTypes as actionTypes, exposedActions as actions };

results matching ""

    No results matching ""