Redux Simple as a rake

    I have already had the opportunity to look into the redux library repository , but from somewhere there was an idea to go deeper into its implementation. In a way, I’d like to share my shocking or even disappointing discovery with the community.

    TL; DR: the redux basic logic is placed in 7 lines of JS code.

    About redux in brief (free translation of the github header):
    Redux is a state management library for applications written in JavaScript.

    It helps to write applications that behave stably / predictably, work on different environments (client / server / native code) and are easily testable.
    I cloned the redux repository , opened the source folder in the editor (ignoring docs , examples , etc.) and took the Delete key to the scissors :

    • Removed all comments from the code.
      Each library method is documented with JSDoc in great detail.
    • Removed the validation and error logging.
      In each method, the input parameters are tightly controlled with very nice looking detailed comments in the console.
    • Removed bindActionCreators , subscribe , replaceReducer, and observable methods .

      ... because I could. Well, or because I was too lazy to write examples for them. But without corner cases, they are even less interesting than what awaits you ahead.

    And now let's sort out what's left



    We write redux for 7 lines


    All redux basic functionality fits into a tiny file for which hardly anyone will create a github repository :)

    function createStore(reducer, initialState) {
        let state = initialState
        return {
            dispatch: action => { state = reducer(state, action) },
            getState: () => state,
        }
    }
    

    Everything. Yes, seriously, EVERYTHING .

    This is how redux works. 18 pages of vacancies on HeadHunter with the search query “redux” - people who hope that you will understand in 7 lines of code. Everything else is syntactic sugar.

    With these 7 lines, you can write TodoApp. Or whatever. But we will quickly rewrite the TodoApp from the documentation for redux.

    // Инициализация хранилища
    function todosReducer(state, action) {
      switch (action.type) {
        case 'ADD_TODO':
          return [
            ...state,
            {
              id: action.id,
              text: action.text,
              completed: false
            }
          ]
        case 'TOGGLE_TODO':
          return state.map(todo => {
            if (todo.id === action.id) {
              return { ...todo, completed: !todo.completed }
            }
            return todo
          })
        default:
          return state
      }
    }
    const initialTodos = []
    const store = createStore(todosReducer, initialTodos)
    // Использование
    store.dispatch({
      type: 'ADD_TODO',
      id: 1,
      text: 'Понять насколько redux прост'
    })
    store.getState() 
    // [{ id: 1, text: 'Понять насколько redux прост', completed: false }]
    store.dispatch({
      type: 'TOGGLE_TODO',
      id: 1
    })
    store.getState() 
    // [{ id: 1, text: 'Понять насколько redux прост', completed: true }]
    

    Already at this stage I thought to throw the microphone from the stage and leave, but show must go on .
    Let's see how the method works.

    combineReducers


    This is a method that allows instead of creating one huge reducer for the entire state of an application at once, breaking it up into separate modules.

    It is used like this:

    // здесь мы переиспользуем метод todosReducer из прошлого примера
    function counterReducer(state, action) {
      if (action.type === 'ADD') {
        return state + 1
      } else {
        return state
      }
    }
    const reducer = combineReducers({
      todoState: todoReducer,
      counterState: counterReducer
    })
    const initialState = {
      todoState: [],
      counterState: 0,
    }
    const store = createStore(reducer, initialState)
    

    Further use of this store can be the same as the previous one.

    The difference of my example and described in the same documentation for TodoApp is quite funny.

    The documentation uses the mods syntax from ES6 (7/8 / ∞):

    const reducer = combineReducers({ todos, counter })
    

    and accordingly rename todoReducer to todos and counterReducer to counter. And many in their code do the same. As a result, there is no difference, but for a person who is familiar with redux, from the first time this thing looks like magic, because the key of the state part (state.todos) corresponds to the function, which is also named only by the developer (function todos () {}) .

    If we had to write such functionality on our micro-redux, we would do this:

    function reducer(state, action) {
      return {
        todoState: todoReducer(state, action),
        counterState: counterReducer(state, action),
      }
    }
    

    This code does not scale well. If we have 2 “sub-states”, we need to write (state, action) twice , and good programmers don’t do that, right?
    In the following example, you are expected not to be afraid of the Object.entries method and the Restructuring of the function parameters
    However, the implementation of the combineReducers method is quite simple (I remind you, this is if you remove validation and error output) and just a little refactor your own taste :

    function combineReducers(reducersMap) {
      return function combinationReducer(state, action) {
        const nextState = {}
        Object.entries(reducersMap).forEach(([key, reducer]) => {
          nextState[key] = reducer(state[key], action)
        })
        return nextState
      }
    }
    

    We have added to our cub redux 9 more lines and a lot of convenience.

    Let's move on to another important feature that seems too complicated to pass by.

    applyMiddleware


    middleware in terms of redux is some kind of thing that listens to all dispatch and does something under certain conditions . Logs, plays sounds, makes requests to the server, ... - something .

    In the original code, the middleware is passed as additional parameters to createStore, but if you don’t spare an extra line of code, then the use of this functionality looks like this:

    const createStoreWithMiddleware = applyMiddleware(someMiddleware)(createStore)
    const store = createStoreWithMiddleware(reducer, initialState)
    

    In this case, the implementation of the applyMiddleware method, when you spend 10 minutes tinkering in someone else's code, comes down to a very simple thing: createStore returns an object with a “dispatch” field. dispatch, as we remember (do not remember) from the first listing of the code, is a function that only applies the reducer to our current state (newState = reducer (state, action)).
    So, applyMiddleware does not override the dispatch method , adding some custom logic before (or after) updating the state.

    Take, for example, the most popular middleware from the creators of redux - redux-thunk

    Its meaning boils down to what can be done not only

    store.dispatch({type: 'SOME_ACTION_TYPE', some_useful_data: 1 })

    but also pass complex functions to store.dispatch

    function someStrangeAction() {
      return async function(dispatch, getState) {
        if(getState().counterState % 2) {
           dispatch({
             type: 'ADD',
           })
        }
        await new Promise(resolve => setTimeout(resolve, 1000))
        dispatch({
          type: 'TOGGLE_TODO',
          id: 1
        })
      }
    }
    

    And now, when we execute the command

    dispatch(someStrangeAction())
    

    then:

    • if the value of store.getState (). counterState is not divisible by 2, it will increase by 1
    • a second after calling our method, todo with id = 1 will switch the completed true to false or vice versa.

    So, I climbed into the redux-thunk repository, and did the same thing as with redux - deleted comments and parameters that extend the basic functionality, but do not change the main one. The

    following happened:

    const thunk = store => dispatch => action => {
      if (typeof action === 'function') {
        return action(store.dispatch, store.getState)
      }
      return dispatch(action)
    }
    

    I understand that the design
    const thunk = store => dispatch => action
    It looks scary, but it also just needs to be called a couple of times with arbitrary parameters and you realize that everything is not so scary, it's just a function that returns a function, a return function (okay, I agree, scary)

    Let me remind you that the original createStore method looked like this

    function createStore(reducer, initialState) {
        let state = initialState
        return {
            dispatch: action => { state = reducer(state, action) },
            getState: () => state,
        }
    }
    

    That is, he accepted the attributes (reducer, initialState) and returned an object with the keys {dispatch, getState}.

    It turned out that implementing the applyMiddleware method is easier than understanding how it works.
    We take the already implemented createStore method and override its return value:

    function applyMiddleware(middleware) {
      return function createStoreWithMiddleware(createStore) {
        return (reducer, state) => {
          const store = createStore(reducer, state)
          return {
            dispatch: action => middleware(store)(store.dispatch)(action),
            getState: store.getState,
          }
        }
      }
    }

    Conclusion


    Under the hood redux contains very simple logical operations. Operations at the level "If gasoline in the cylinder lights up, the pressure increases." But then, can you build on these concepts a Formula 1 car - decide for yourself.

    PS


    To add a simplified store.subscribe method to my “micro-redux”, it took 8 lines of code. And you?

    Also popular now: