Реактивные приложения (:

Что значит "реактивное"?О_о

Вы и так знаете что это такое (;

Flux

React

Rx

Redux

Приступим к делу!

Что хотим от приложения?

  • Заполнять матрицы
  • Менять размер матриц
  • Складывать две матрицы
  • Складывать три матрицы!
  • Нет четыре!

Матрица на JS

const matrix_A = [
    [0, 1, 2, 0, 1], 
    [0, 1, 1, 0, 0],
    [0, 1, 1, 0, 0],
    [0, 1, 3, 1, 1]];

Пишем тесты:

import {expect} from 'chai';

import {initMatrix, setCellValue, setCols, setRows} from '../src/source/core.js';

describe('Matrix actions', () => {
    it('should set value in matrix cell', () => {
        const cols = 2;
        const rows = 2;
        const matrix = initMatrix(cols, rows);
        expect(matrix).to.equal([
            [0,0],
            [0,0],
        ]);
    })

    it('should set value in matrix cell', () => {
        const matrix = [
            [0,0],
            [0,0],
        ];
        const changedMatrix = setCellValue(matrix, 0, 1, 12);
        expect(changedMatrix).to.equal([
            [0,12],
            [0,0],
        ])
    })
})

Затем методы

// '/src/source/core.js';

export const initMatrix = (rows, cols) => Array(rows).fill(Array(cols).fill(0));

export const setCellValue = (matrix, row, col, value) =>
  matrix.map((rowValue, rowIndex) => {
    if (rowIndex !== row) return rowValue;
    return rowValue.map((colValue, colIndex) => {
      return colIndex === col ? value : colValue;
    })
  });

Затем тесты

describe('Matrix actions', () => {
    /* ... */
    it('should increase matrix column numbers', () => {
        const matrix = [
            [1, 0],
            [0, 1],
        ];
        const changedMatrix = setCols(matrix, 4);
        expect(changedMatrix).to.equal([
            [1, 0, 0, 0],
            [0, 1, 0, 0],
        ]);
    })

    it('should increase matrix rows numbers', () => {
        const matrix = [
            [1, 0],
            [0, 1],
        ];
        const changedMatrix = setRows(matrix, 4);
        expect(changedMatrix).to.equal([
            [1, 0],
            [0, 1],
            [0, 0],
            [0, 0],
        ]);
    })
})

И еще тесты

describe('Matrix actions', () => {
    /* ... */
    it('should decrease matrix column numbers', () => {
        const matrix = [
            [1, 0, 0, 0],
            [0, 1, 0, 0],
        ];
        const changedMatrix = setCols(matrix, 2);
        expect(changedMatrix).to.equal([
            [1, 0],
            [0, 1],
        ]);
    })

    it('should decrease matrix rows numbers', () => {
        const matrix = [
            [1, 0],
            [0, 1],
            [0, 0],
            [0, 0],
        ];
        const changedMatrix = setRows(matrix, 2);
        expect(changedMatrix).to.equal([
            [1, 0],
            [0, 1],
        ]);
    })
})

Потом методы

// src/source/core.js

// тут все просто
export const setRows = (matrix, rows) => {
    const res = [];
    for (let rowIndex = 0; rowIndex < rows; rowIndex ++) {
        const row = [];
        for (let colIndex = 0; colIndex < matrix[0].length; colIndex++) {
            row.push(matrix[rowIndex] ? matrix[rowIndex][colIndex] : 0);
        }
        res.push(rows);
    }
    return res;
}

export const setCols = (matrix, cols) => {
    const res = [];
    for (let rowIndex = 0; rowIndex < matrix.length; rowIndex ++) {
        const row = [];
        for (let colIndex = 0; colIndex < cols; colIndex++) {
            row.push(matrix[rowIndex][colIndex] || 0);
        }
        res.push(rows);
    }
    return res;
}

Да сколько можно тестов писать?

describe('Matrix actions', () => {
    /* ... */
  it('should summarize 4 matrices', () => {
    const matrix_A = [
      [1, 0],
      [0, 1],
    ];
    const matrix_B = [
      [1, 2],
      [3, 1],
    ];
    const matrix_C = [
      [1, 0],
      [0, 1],
    ];
    const matrix_E = [
      [1, 2],
      [3, 1],
    ];
    const result = matrixSum(matrix_A, matrix_B, matrix_C, matrix_E);
    expect(result).to.deep.equal([
      [4, 4],
      [6, 4],
    ]);
  });
})

Надеюсь последний метод

const summarize = (matrix_A, matrix_B) =>
  matrix_A.map((row, rowIndex) =>
    row.map(
      (value_A, colIndex) => value_A + matrix_B[rowIndex][colIndex]
    )
  );

export const matrixSum = (matrices) => {
  return matrices.length ? matrices.reduce(summarize) : [];
}

Костяк готов!
Заставим это работать!

Нам надо где-то хранить матрицы и результат вычисления

const state = {
    rows: 3,
    cols: 3, 
    matrices: [],
    result: [],
}

Как будет работать приложение

INIT_MATRIX -> app -> next state with computed result

SET_ROWS -> app -> next state with computed result

SET_CELL_VALLUE -> app -> next state with computed result
const actions = [
  {type: INIT_MATRIX},
  {type: INIT_MATRIX},
  {type: SET_ROWS, rows: 3},
  {type: SET_COLS, cols: 4},
  {type: SET_CELL_VALUE, id: 0, row: 0, col: 0, value: 5},
  {type: SET_CELL_VALUE, id: 0, row: 1, col: 2, value: 10},
  {type: SET_CELL_VALUE, id: 0, row: 2, col: 2, value: 6},
  {type: SET_CELL_VALUE, id: 1, row: 0, col: 0, value: 1},
  {type: SET_CELL_VALUE, id: 1, row: 1, col: 1, value: 3},
  {type: SET_CELL_VALUE, id: 1, row: 2, col: 1, value: 4},
  {type: INIT_MATRIX},
  {type: SET_CELL_VALUE, id: 2, row: 0, col: 0, value: 6},
  {type: SET_CELL_VALUE, id: 2, row: 0, col: 1, value: 8},
  {type: SET_CELL_VALUE, id: 2, row: 2, col: 0, value: 12},
];

const resultState = actions.reduce(reducer, INITIAL_STATE);
    expect(resultState.result).to.deep.equal([
      [12, 8, 0, 0],
      [0, 3, 10, 0],
      [12, 4, 6, 0],
    ]);

Reducer?

const reducer = (state, action) => next state
import {initMatrix} from './core';

const INITIAL_STATE = {
  rows: 3,
  cols: 3,
  matrices: [],
  result: [],
}

export default (state = INITIAL_STATE, action) => {
    let nextState = state;
    if (action.type === 'INIT_MATRIX') 
        nextState = {
            ...state,
            matrices: state.matrices.concat([initMatrix(state.rows, state.cols)])
        }

    return nextState;
}
// Spread

var obj = {a: 1, b: 2, c: 3, d: 4};

var {a} = obj; // тоже самое что и var a = obj.a

var {a, ...other} = obj; 

other // {b: 2, c: 3, d: 4};

var b = {a, ...other} // {a: 1, b: 2, c: 3, d: 4}



var arr = [1, 2, 3, 4];

var [a] = arr; // тоже что и var a = arr[0];

var [a, ...other] = arr; 

other // [2, 3, 4];

var b = [a, ...other] // [1, 2, 3, 4];
import {initMatrix} from './core';

export const INITIAL_STATE = {
  rows: 3,
  cols: 3,
  matrices: [],
  result: [],
}

export default (state = INITIAL_STATE, action) => {
    let nextState = state;
    if (action.type === 'INIT_MATRIX') 
        nextState = {
            ...state,
            matrices: [...state.matrices, initMatrix(state.rows, state.cols)]
        }

    return nextState;
}

Забыли про тесты :(

import {expect} from 'chai';
import reducer from 'reducer';

const INITIAL_STATE = {
  rows: 2,
  cols: 2,
  matrices: [],
  result: [],
}

describe('Reducer test', () => {
    it('should add matrix to matrices list', () => {
        const action = {type: 'INIT_MATRIX'};
        const nextState = reducer(INITIAL_STATE, action);
        expect(nextState).to.deep.equal({
            rows: 2,
            cols: 2,
            matrices: [
                [[0, 0],
                [0, 0]],
            ],
            result: []
        })
    })
})
export default (state = INITIAL_STATE, action) => {
  let nextState;
  switch (action.type) {
    case INIT_MATRIX:
      nextState = {
        ...state,
        matrices: [...state.matrices, initMatrix(state.rows, state.cols)]
      }; break;

    case SET_CELL_VALUE:
      const matrix = state.matrices[action.id];
      nextState = {
        ...state,
        matrices: [
          ...state.matrices.slice(0, action.id),
          setCellValue(matrix, action.row, action.col, action.value),
          ...state.matrices.slice(action.id + 1)
        ]
      }; break;
    default:
      nextState = state;
  }
  return {
    ...nextState,
    result: calcMatrices(nextState.matrices, summarize)
  };
}
export default (state = INITIAL_STATE, action) => {
  let nextState;
  switch (action.type) {
    case INIT_MATRIX:
      nextState = {
        ...state,
        matrices: [...state.matrices, initMatrix(state.rows, state.cols)]
      }; break;

    case SET_CELL_VALUE:
      const matrix = state.matrices[action.id];
      nextState = {
        ...state,
        matrices: [
          ...state.matrices.slice(0, action.id),
          setCellValue(matrix, action.row, action.col, action.value),
          ...state.matrices.slice(action.id + 1)
        ]
      }; break;

    case SET_ROWS:
      nextState = {
        ...state,
        rows: action.rows,
        matrices: state.matrices.map(matrix => setRows(matrix, action.rows))
      }; break;

    case SET_COLS:
      nextState = {
        ...state,
        cols: action.cols,
        matrices: state.matrices.map(matrix => setCols(matrix, action.cols))
      }; break;

    default:
      nextState = state;
  }
  return {
    ...nextState,
    result: calcMatrices(nextState.matrices, summarize)
  };
}
import reducer from 'reducer.js';

const actions = [
  {type: INIT_MATRIX},
  {type: INIT_MATRIX},
  {type: SET_ROWS, rows: 3},
  {type: SET_COLS, cols: 4},
  {type: SET_CELL_VALUE, id: 0, row: 0, col: 0, value: 5},
  {type: SET_CELL_VALUE, id: 0, row: 1, col: 2, value: 10},
  {type: SET_CELL_VALUE, id: 0, row: 2, col: 2, value: 6},
  {type: SET_CELL_VALUE, id: 1, row: 0, col: 0, value: 1},
  {type: SET_CELL_VALUE, id: 1, row: 1, col: 1, value: 3},
  {type: SET_CELL_VALUE, id: 1, row: 2, col: 1, value: 4},
  {type: INIT_MATRIX},
  {type: SET_CELL_VALUE, id: 2, row: 0, col: 0, value: 6},
  {type: SET_CELL_VALUE, id: 2, row: 0, col: 1, value: 8},
  {type: SET_CELL_VALUE, id: 2, row: 2, col: 0, value: 12},
];

describe('It should work!', () => {
  it('SHOULD WORK!', () => {
    const result = actions.reduce(reducer, INITIAL_STATE);
    expect(result.result).to.deep.equal([
      [12, 8, 0, 0],
      [0, 3, 10, 0],
      [12, 4, 6, 0],
    ]);
  });
});

Добавим больше экшена

// SET_CELL_VALUE

{type: 'SET_CELL_VALUE', id: 0, row: 1, col: 2, value: 12} 

// Action creator

const cellCellValue = (id, row, col, value) =>
    ({type: 'SET_CELL_VALUE', id, row, col, value} )


// actions.js

export const initMatrix = () => ({type: 'INIT_MATRIX'});

export const setCellValue = (id, row, col, value) => 
  ({type: 'SET_CELL_VALUE', id, row, col, value});

export const setCols = (cols) => {
  if (cols === 0) return;
  return {type: 'SET_COLS', cols};
};

export const setRows = (rows) => {
  if (rows === 0) return;
  return {type: 'SET_ROWS', rows};
};

Синглтон

  • Хранит состояние
  • Отдает состояние
  • Изменяет состояние при получении экшенов
  • Сообщает о том, что изменилось состояние
export default function (reducer, initialState) {
  let state = initialState;
  let listeners = [];

  const getState = () => state;

  const dispatch = action => {
    state = reducer(state, action);
    listeners.forEach(listener => listener())
  };

  const subscribe = listener => {
    listeners.push(listener);
    return () => {
      listener = listeners.filter(l => l !== listener);
    }
  };

  dispatch({});

  return {getState, dispatch, subscribe}
}
import createStore from './source/store';
import reducer from './source/reducer';
import INITIAL_STATE from './source/default_state';
import createApp from './ui/app';

const store = createStore(reducer, INITIAL_STATE);

const app = createApp(store.getState(), store.dispatch);
const render = () => app.render(document.getElementById('app'));

store.subscribe(render);
render();

Action

Reducer

View

State

Что в итоге?

  • Только "чистые" функции
  • Нет никакой неявности
  • Тестируемость
  • Легко поддерживать

Вернемся к Redux'у

  • Actions
  • Reducers
  • Middlewares
  • State

Reducers

export default (state = INITIAL_STATE, action) => {
  let nextState;
  switch (action.type) {
    case INIT_MATRIX:
      nextState = {
        ...state,
        matrices: [...state.matrices, initMatrix(state.rows, state.cols)]
      }; break;

    case SET_CELL_VALUE:
      const matrix = state.matrices[action.id];
      nextState = {
        ...state,
        matrices: [
          ...state.matrices.slice(0, action.id),
          setCellValue(matrix, action.row, action.col, action.value),
          ...state.matrices.slice(action.id + 1)
        ]
      }; break;
    default:
      nextState = state;
  }
  return {
    ...nextState,
    result: calcMatrices(nextState.matrices, summarize)
  };
}

+ Immutable.js

export default (state = INITIAL_STATE, action) => {
  let nextState;
  switch (action.type) {
    case INIT_MATRIX:
      nextState = state.update(
        'matrices', 
        matrices => matrices.concat(initMatrix, state.get('rows'), state.get('cols'))
      );

    case SET_CELL_VALUE:
      nextState = state.update(
        'matrices', 
        matrices => matrices.setIn([action.id, action.row, action.col], action.val)
      );break;

    default:
      nextState = state;
  }

  return nextState.set('result', calcMatrices(nextState.get('matrices'), summarize));
}

Reducer creators

export default createReducer({

    INIT_MATRIX: (state, action) => state.update(
        'matrices', 
        matrices => matrices.concat(initMatrix, state.get('rows'), state.get('cols'))
      ),

    SET_CELL_VALUE: (state, action) => state.update(
        'matrices', 
        matrices => matrices.setIn([action.id, action.row, action.col], action.val)
      );break;

}, INITIAL_STATE)

Actions

Action creators

const putUser = (id) => ({type: 'PUT_USER', id});
const deleteUser = (id) => ({type: 'DELETE_USER', id});
const getUser = (id) => ({type: 'GET_USER', id});
const PUT_USER = 'PUT_USER';
const DELETE_USER = 'DELETE_USER';
const GET_USER = 'GET_USER'

const putUser = (id) => ({type: PUT_USER, id});
const deleteUser = (id) => ({type: DELETE_USER, id});
const getUser = (id) => ({type: GET_USER, id});

Middlewares

State

Action

Reducer

Middleware

New action

View

Promise middleware

export default function promiseMiddleware() {
  return next => action => {
    const { promise, type, ...rest } = action;

    if (!promise) return next(action);

    const SUCCESS = type;
    const REQUEST = type + '_REQUEST';
    const FAILURE = type + '_FAILURE';
    next({...rest, type: REQUEST});
    return promise
      .then(res => {
        next({...rest, res, type: SUCCESS});
        return true;
      })
      .catch(error => {
        next({...rest, error, type: FAILURE});
        console.error(error.stack)
        return false;
      });

  };
}
export const putUser = (id) => ({
    type: PUT_USER,
    promise: api.putUser(id),
    id
})
{
    type: PUT_USER,
    res,
    id
}

{
    type: PUT_USER_FAILURE,
    error
}
{
    type: PUT_USER_REQUEST
}

Api middleware

export const getUserById = (id) => {
  return {
    [CALL_API]: {
      types: [ REQUISITES_REQUEST, RECEIVES_REQUISITES, REQUISITES_REQUEST_FAILED ],
      payload: [id],
      entity: ENTITIES.USER,
      method: 'GET',
    },
  };
};
{
    type: REQUISITES_REQUEST
}

{
    type: RECEIVES_REQUISITES,
    response
}

{
    type: REQUISITES_REQUEST_FAILED,
    error
}

Redux slider monitor


Made with Slides.com