ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Redux
    Framework/SPA 2020. 7. 16. 00:36

    1. Overview

    Redux is a predictable state container that first appeared in JavaScript land. Urunium.Redux is an opinionated Redux implementation for .net core written in c#. This implementation is more geared towards being c# OOP oriented instead of functional oriented, while still adhering to Redux core principles. As such the programming models and APIs used would be more familiar to a c# developer.

    At its core redux is an implementation of Flux model which advocates one-way data flow, to make application state highly predictable. The following gif does summarize how data flows in redux:

    Although there is a small issue with this image. Redux as a concept doesn't have "Dispatcher", redux has "Dispatch" api exposed through Store object.

    2. Principle of Redux

    2.1 Single Source of truth

    The global state of your application is stored in an object tree within a single store.

    This makes it easy to create universal apps, as the state from your server can be serialized and hydrated into the client with no extra coding effort. A single state tree also makes it easier to debug or inspect an application; it also enables you to persist your app's state in development, for a faster development cycle. Some functionality that has been traditionally difficult to implement - Undo/Redo, for example - can suddenly become trivial to implement, if all of your states are stored in a single tree.

    console.log(store.getState())
    
    /* Prints
    {
      visibilityFilter: 'SHOW_ALL',
      todos: [
        {
          text: 'Consider using Redux',
          completed: true,
        },
        {
          text: 'Keep all state in a single tree',
          completed: false
        }
      ]
    }
    */

    2.2 State is read-only

    The only way to change the state is to emit an action, an object describing what happened.

    This ensures that neither the views nor the network callbacks will ever write directly to the state. Instead, they express an intent to transform the state. Because all changes are centralized and happen one by one in a strict order, there are no subtle race conditions to watch out for. As actions are just plain objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes.

    store.dispatch({
      type: 'COMPLETE_TODO',
      index: 1
    })
    
    store.dispatch({
      type: 'SET_VISIBILITY_FILTER',
      filter: 'SHOW_COMPLETED'
    })

    2.3 Changes are made with pure functions

    To specify how the state tree is transformed by actions, you write pure reducers.

    Reducers are just pure functions that take the previous state and an action and return the next state. Remember to return new state objects, instead of mutating the previous state. You can start with a single reducer, and as your app grows, split it off into smaller reducers that manage specific parts of the state tree. Because reducers are just functions, you can control the order in which they are called, pass additional data, or even make reusable reducers for common tasks such as pagination.

    function visibilityFilter(state = 'SHOW_ALL', action) {
      switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
          return action.filter
        default:
          return state
      }
    }
    
    function todos(state = [], action) {
      switch (action.type) {
        case 'ADD_TODO':
          return [
            ...state,
            {
              text: action.text,
              completed: false
            }
          ]
        case 'COMPLETE_TODO':
          return state.map((todo, index) => {
            if (index === action.index) {
              return Object.assign({}, todo, {
                completed: true
              })
            }
            return todo
          })
        default:
          return state
      }
    }
    
    import { combineReducers, createStore } from 'redux'
    const reducer = combineReducers({ visibilityFilter, todos })
    const store = createStore(reducer)

    3. Component

    3.1 Store

    Store in redux is a global singleton that holds the latest state of the application. Unlike original flux implementation, redux proposes use of a single object tree to represent the whole of the application's state instead of using multiple stores. In Urunium.Redux, store implements IStore<TState> interface, where TState is the class representing the state.

    var rootReducer = new ReducerComposer();
    var initialState = new AppState();
    var store = new Store<AppState>(rootReducer, initialState);

    3.2 State

    State object holds the current state of the application. The state mainly represents the current status of UI and also holds few data helping in business logic. In Urunium.Redux state can be any type: a class, struct, or primitive datatype. There is just one rule that states must follow: Immutability. A state object will never change its properties. Every time we need to change something a new instance of state must be created and a full old state must be replaced by this new object.

    3.3 Props

    Props refers to the value that the child component receives as a parameter from the parent component and it is immutable.

    3.4 Reducers

    Reducers are pure function whose name comes from "reduce()" method of functional programming (as in map/reduce). In Urunium.Redux reducer is a class that implements IReducer<TState> interface. It has only one method named Apply(). The signature of the Apply method is as follows:

    //IReducer<TState>.Apply:
    TState Apply (TState previousState, Object action)

    Here Apply must be a "pure" function, i.e. it should always return the same result for the same input parameters, any number of times it is invoked.

    class IncrementAction { }
    class DecrementAction { }
    class Counter : IReducer<int>
    {
        public int Apply(int previousState, object action)
        {
            switch (action)
            {
                case IncrementAction inc:
                    return previousState + 1;
                case DecrementAction dec:
                    return previousState - 1;
            }
            // Unsupported actions should return previousState unchanged.
            return previousState;
        }
    }

    3.5 Root Reducer

    An application using Urunium.Redux can divide it's reducer into multiple classes to achieve higher maintainability. But all these reducers must ultimately be composed into a single reducer. The root reducer works by broadcasting any action it receives to all the registered sub-reducers. Whichever sub-reducer can handle action will handle it, or return previousState as-is if it cannot handle the action.

    // A quick example for implementing own root-reducer composed of sub-reducers.
    
    class IncrementAction { }
    class DecrementAction { }
    class Counter : IReducer<int>
    {
        private List<IReducer<int>> _subReducers = new List<IReducer<int>> { new IncrementReducer(), new DecrementReducer() };
    
        public int Apply(int previousState, object action)
        {
            // Broadcast
            foreach(var reducer in _subReducers)
            {
                previousState = reducer.Apply(previousState, action);
            }
    
            return previousState;
        }
    }
    class IncrementReducer : IReducer<int>
    {
        public int Apply(int previousState, object action)
        {
            if (action is IncrementAction)
            {
                return previousState + 1;
            }
            // Unsupported actions should return previousState unchanged.
            return previousState;
        }
    }
    class DecrementReducer : IReducer<int>
    {
        public int Apply(int previousState, object action)
        {
            if (action is DecrementAction)
            {
                return previousState - 1;
            }
            // Unsupported actions should return previousState unchanged.
            return previousState;
        }
    }

    3.6 ReducerComposer

    ReducerComposer<TState> in Urunium.Redux is a special class meant to be used as the "Root Reducer". It helps in segregating our reducers by action or property (of application state). To divide reducers by action use:

    Urunium.Redux.Compose.ReducerComposer<TState> AddStateReducer (Urunium.Redux.IReducer<TState> stateReducer)

    method.

    And to divide reducers by application state's property use:

    Urunium.Redux.Compose.ReducerComposer<TState> AddSubTreeReducer <TPart>(Urunium.Redux.Compose.ISubTreeReducer<TState, TPart> subTreeReducer)

    3.7 TypedReducer

    TypedReducer in Urunium.Redux makes it easy to write strongly typed reducers. Instead of dealing with object actions, we can create a reducer that deals with a specific type. It is somewhat like dividing reducer by action, but instead dividing into classes, we divided into functions.

    public class Counter : Typed.TypedReducer<int>
    {
       public int Apply(int previousState, Increment action)
       {
           return previousState + 1;
       }
    
       public int Apply(int previousState, Decrement action)
       {
           return previousState - 1;
       }
    }

    We can also optionally implement IApply interfaces in typed reducers e.g.

    public class Counter : Typed.TypedReducer<int>, IApply<int, Increment>, IApply<int, Decrement>
    {
       public int Apply(int previousState, Increment action)
       {
           return previousState + 1;
       }
    
       public int Apply(int previousState, Decrement action)
       {
           return previousState - 1;
       }
    }

    3.8 Sub-tree reducer

    A sub-tree reducer is any reducer that implements ISubTreeReducer interface. These reducers work with a property of application's state instead of working with full state. ReducerComposer<TState> can be used to compose multiple sub-tree reducers into single root-reducer. A special helper class SubTreeToFullTreeAdapter is internally used by ReducerComposer to translate the effect of sub-tree reducer into the application's main state. If anyone wants to write custom reducer-composer this may be helpful.

    Sample:

    The following sample shows usage of sub-tree reducer, typed reducer, and reducer-composer.

    // State:
    class Todo
    {
        public Filter VisibilityFilter { get; }
        public List<TodoItem> Todos { get; }
    
        public Todo(Filter visibilityFilter, List<TodoItem> todos)
        {
            VisibilityFilter = visibilityFilter;
            Todos = todos;
        }
    }
    
    class TodoItem
    {
        public string Text { get; }
        public bool IsComplete { get; }
        public TodoItem(string text, bool isComplete)
        {
            Text = text;
            IsComplete = isComplete;
        }
    }
    
    enum Filter
    {
        ShowAll,
        Active,
        Inactive
    }
    
    // Actions:
    class SetVisibilityFilter
    {
        public Filter Filter { get; }
        public SetVisibilityFilter(Filter filter)
        {
            Filter = filter;
        }
    }
    
    class AddTodo
    {
        public string Text { get; }
        public AddTodo(string text)
        {
            Text = text;
        }
    }
    
    class ToggleTodo
    {
        public int Index { get; }
        public ToggleTodo(int index)
        {
            Index = index;
        }
    }
    
    // Reducers:
    class TodosReducer : TypedReducer<List<TodoItem>>, ISubTreeReducer<Todo, List<TodoItem>>
    {
        public Expression<Func<Todo, List<TodoItem>>> PropertySelector => state => state.Todos;
    
        public List<TodoItem> Apply(List<TodoItem> previousState, AddTodo action)
        {
            return previousState.Concat(new[] {
                new TodoItem(action.Text, false)
            }).ToList();
        }
    
        public List<TodoItem> Apply(List<TodoItem> previousState, ToggleTodo action)
        {
            return previousState.Select((todoItem, i) =>
            {
                if (action.Index == i)
                {
                    return new TodoItem(todoItem.Text, !todoItem.IsComplete);
                }
                return todoItem;
            }).ToList();
        }
    }
    
    class VisibilityFilterReducer : TypedReducer<Filter>, ISubTreeReducer<Todo, Filter>
    {
        public Expression<Func<Todo, Filter>> PropertySelector => state => state.VisibilityFilter;
    
        public Filter Apply(Filter previousState, SetVisibilityFilter action)
        {
            return action.Filter;
        }
    }
    
    // Usage:
    var rootReducer = new ReducerComposer<Todo>().AddSubTreeReducer(new TodosReducer()).AddSubTreeReducer(new VisibilityFilterReducer());
    var initialState = new Todo(Filter.All, new List<TodoItem>());
    var store = new Store<Todo>(rootReducer, initialState);

    3.9 Actions

    Actions are objects telling reducer what needs to be done to the current state. Reducer decides operation to be performed on a state based on the type of action that is dispatched. Optionally actions may also contain payloads that reducer can use to make further decisions. In Urunium.Redux action can be any object including primitive type. In general, it is a good idea to make actions immutable too.

    4. Reference

    https://uruniumredux.readthedocs.io/en/latest/

    https://redux.js.org/introduction/three-principles

    'Framework > SPA' 카테고리의 다른 글

    Mobx 6  (0) 2020.12.01
    Webpack  (0) 2020.05.25
    Comparison AngularJS, ReactJS, and Vue.JS  (0) 2020.04.11
    Comparison between AngularJS and ReactJS  (0) 2020.04.10
    Two-way binding  (0) 2020.04.10

    댓글

Designed by Tistory.