Redux для самых маленьких: как создать свою библиотеку

Статья написана студентом Хекслета. Мнение автора может не совпадать с позицией редакции
Читать в полной версии →

Всем привет!

Этой мой первый пост в дневнике, и посвящён он библиотеке Redux.

За время моего обучения на Hexlet именно эта библиотека породила кучу вопросов а-ля -'как это работает, почему это так работает ?'. Несколько дней я перечитывал теорию по Redux, но в голове не укладывалось, пока я не написал свою собственную версию Redux.

Предлагаю вам вместе со мной написать свою версию библиотеки, просто чтобы разобраться как это работает "под капотом".

В Redux ядром всего является объект store, который возвращает нам функция createStore(). Поэтому давайте начнём с создания этой функции.

Согласно документации createStore принимает первым аргументов функцию reducer, а вторым начальное состояние.

const createStore = (reducer, initialState) => {
  let state = reducer(initialState, { type: '__INIT__' });
  let subscribers = [];

  return {
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach((cb) => cb());
    },
    subscribe(cb) {
      subscribers = [...subscribers, cb];
    },
    getState() {
      return state;
    },
  };
};

Сначала инициализируем state начальными данными.

let state = reducer(initialState, { type: '__INIT__' });

Функцию reducer мы напишем позже, но главное понимать, что функция возвращает измененный или неизменённый state.

Функция createStore должна вернуть объект store со следующими методами: dispatch(), subscribe(), getState().

getState() просто возвращает текущий state.

subscribe() принимает в качестве аргумента callback-функцию и складывает функцию в массив.

dispatch() принимает в качестве аргумента объект. Этот объект может иметь внутри любые поля и данные, но в обязательном порядке должен иметь поле type. Например { type: 'ADD' }. Когда вызывается метод dispatch(), то state должен быть изменен через reducer. Reducer - это функция, внутри которой данные меняются, в зависимости от описанной внутри логики. После того, как state был изменен, нам нужно последовательно вызвать все callback-функции, которые были добавлены в массив посредством метода subscribe().

Теперь создаем функцию reducer, которая согласно внутренней логики будет менять состояние.

const ourReducer = (state, action) => {
  switch(action.type) {
    case 'INC':
      return state + 1;
    case 'DEC':
      return state - 1;
    default:
      return state;
  }
};

Здесь все должно быть понятно. Функция принимает объект action с полем type. Например { type: 'INC }. В зависимости от значения поля type отрабатывает switch-case конструкция и происходит возврат состояния.

Дальше создаем объект store, интерфейсами которого и будем манипулировать.

const store = createStore(ourReducer, 0);

Теперь попробуем вызвать методы нашего хранилища store. Для начала мы хотим чтобы при изменении нашего состояния в console автоматически печаталось текущее состояние.

store.subscribe(() => console.log(store.getState())); 

Теперь вернем текущее состояние

store.getState(); // -> вернётся 0, так как это начальное состояние.

Теперь изменим наше состояние методом dispatch(), который внутри вызывает редьюсер, меняющий состояние, и вызывает последовательно все callback функции из массива subscribers.

store.dispatch({ type: 'INC' }); // -> в консоле будет 1
store.dispatch({ type: 'INC' }); // -> в консоле будет 2
store.dispatch({ type: 'DEC' }); // -> в консоле будет 1 

Как оказалось, ничего сложного здесь нет. Надеюсь мои эксперименты кому-то помогут быстрее вникнуть в то, как работает библиотека Redux.

Ниже представлен полный код для удобства копирования в свой редактор:

const createStore = (reducer, initialState) => {
  let state = reducer(initialState, { type: '__INIT__' });
  let subscribers = []; 

  return {
    dispatch(action) {
      state = reducer(state, action);
      subscribers.forEach((cb) => cb());
    },
    subscribe(cb) {
      subscribers = [...subscribers, cb];
    },
    getState() {
      return state;
    },
  };
};

const ourReducer = (state, action) => {
  switch(action.type) {
    case 'INC':
      return state + 1;
    case 'DEC':
      return state - 1;
    default:
      return state;
  }
};

const store = createStore(ourReducer, 0);

store.subscribe(() => console.log(store.getState())); 
store.getState(); 
store.dispatch({ type: 'INC' }); 
store.dispatch({ type: 'INC' }); 
store.dispatch({ type: 'DEC' });