/** @module */ /** * state.js * * The application uses Redux to handle the state. * Redux has a **store** which allows access to the one big state of the * modules. It also has a dispatcher, which receives actions and calls * reducer functions to act on them. * * The store handles all state changes. For that, a module * defines **action creators**, which are helper functions generating action * objects. An action object contains a 'type' property, which gives the action * a name, and a payload, which will be processed. * * The processing unit is called a **reducer** and is exported as 'reducer' in * this file. It is a function which receives * the current state and an action and returns the processed state. It is good * practice, that the reducer doesn't change the state (which is passed by * reference), but creates a modified state and returns it. Also, the reducer * should return immediately. If an asynchronous operation has to be executed, * it is handled through redux-sagas, which is basically a concurrent process * triggered by a reducer, which generates a new action once it's completed. * * Action creators (defined as 'actions' in this file) are bound to the * Redux dispatcher in 'index.js'. When they are called, the dispatcher sends * the greated action to **all** reducers. This module, and other modules which * change the state of this module, use these action creators to modifiy the * state. * * This file exports everything which is necessary to set up and operate the * state of this module. * - actions * - reducers * - state * - sagas **/ // Import NAME and DATA to be able to create action creators */ import { NAME, DATA } from './constants' import { primary } from './initialData' import { delay } from 'redux-saga' import { call, put, takeEvery } from 'redux-saga/effects' /** actionTypes define what actions are handeled by the reducer. */ const actionTypes = {} export const actionCreators = {} // Generate default actionsTypes CREATE, UPDATE, REMOVE DATA.forEach((dataItem, idx) => { ['create', 'update', 'remove'].forEach(action => { // The Redux convention is to name action types e.g. demo_module/UPDATE // For action creators, we define here the name e.g. removePrimary(id, data) // where id is the element id and data is the element itself. const actionType = `${action.toUpperCase()}_${dataItem.toUpperCase()}` const actionName = `${action}${dataItem[0].toUpperCase()}${dataItem.substring(1)}` if (idx === 0) { actionCreators[actionName] = (id, data) => { return { type: `${NAME}/${actionType}`, id, data } } } else { actionCreators[actionName] = (primaryId, id, data) => { return { type: `${NAME}/${actionType}`, primaryId, id, data } } } actionTypes[actionType] = `${NAME}/${actionType}` }) }) // Add specific action creators here: actionCreators['loadSamples'] = () => { return { type: `${NAME}/LOAD_SAMPLES` } } actionTypes['LOAD_SAMPLES'] = `${NAME}/LOAD_SAMPLES` console.log('State actionTypes, actionCreators', actionTypes, actionCreators) /** state definition */ /** It is generally easier to not have another object here. */ export const state = [] console.log('State state', state) /** reducer is called by the redux dispatcher and handles all component actions */ /** The module's root reducer tries to split up the state and let specific * specialized reducers handle the work. This is not necessary, but more * convenient and readable. */ // In this example it is assumed, that the secondary items have a key 'primaryId', // which references the primary data element. export function reducer (state = [], action) { // Return immediately, if action type doesn't match if (typeof action.type === 'undefined' || !action.type.match(RegExp(NAME))) { return state } // find the primary data element with the matching id. let idx let nextState console.log(`Entering demo_module root reducer.`, state, action, idx) if (action.type.match(/SECONDARY/)) { idx = state.findIndex(elem => { return (action.primaryId === elem.id) }) // leave immediately, if no idx was found (operations on secondary not valid if there's no primary) if (idx < 0) { return state } console.log(`Using secondary reducer.`) const subState = state[idx].secondary const reducedState = secondaryReducer(subState, action) nextState = [ ...state.slice(0, idx), {...state[idx], secondary: reducedState}, ...state.slice(idx + 1) ] console.log('Leaving demo_module root reducer', subState, reducedState, nextState) return nextState } switch (action.type) { case actionTypes.CREATE_PRIMARY: nextState = [ ...state, action.data ] console.log('Creating primary', state, nextState) return nextState case actionTypes.UPDATE_PRIMARY: nextState = [ ...state.slice(0, idx), action.data, ...state.slice(idx + 1) ] console.log('Updating primary', state, nextState) return nextState case actionTypes.REMOVE_PRIMARY: console.log('wtf') nextState = [ ...state.slice(0, idx), ...state.slice(idx + 1) ] console.log('Removing primary', state, nextState) return nextState case actionTypes.LOAD_SAMPLES: nextState = primary console.log('Loading sample data', state, nextState) return nextState default: return state } } function secondaryReducer (state = [], action) { console.log(`Entering secondary reducer.`, state, action) const idx = state.findIndex(elem => { console.log('searching index', action, elem) return (action.id === elem.id) }) let nextState switch (action.type) { case actionTypes.CREATE_SECONDARY: nextState = [ ...state, action.data ] console.log(`Creating secondary.`, state, nextState) return nextState case actionTypes.UPDATE_SECONDARY: nextState = [ ...state.slice(0, idx), action.data, ...state.slice(idx + 1) ] console.log(`Updating secondary.`, idx, state, nextState) return nextState case actionTypes.REMOVE_SECONDARY: nextState = [ ...state.slice(0, idx), ...state.slice(idx + 1) ] console.log(`Removing secondary.`, idx, state, nextState) return nextState default: return state } } console.log('State reducer', reducer) /** sagas are asynchronous workers (JS generators) to handle the state. // Worker export function * incrementAsync () { try { const data = yield call(Api.isIncrementOk) yield put({ type: 'INCREMENT_SUCCESS', data }) } catch (error) { yield put({ type: 'INCREMENT_FAIL', error}) } } // Watcher (intercepts INCREMENT_REQUEST, dispatches INCREMENT_SUCCESS or INCREMENT_FAIL in return.) export function * watchIncrementAsync () { yield takeEvery('INCREMENT_REQUEST', incrementAsync) } // export all sagas in parallel function * sagas () { yield takeEvery('INCREMENT_REQUEST') yield takeEvery('DECREMENT_REQUEST') } */ export const sagas = null console.log('State sagas', sagas)