📜 ⬆️ ⬇️

Do you really need Redux?

Not so long ago, React positioned itself as "V in MVC". After this commit, the marketing text changed, but the essence remained the same: React is responsible for the display, the developer - for everything else, that is, speaking in MVC terms, for Model and Controller.


One of the solutions for managing the Model (state) of your application is Redux. Its appearance is motivated by the increased complexity of frontend applications that MVC cannot handle.


Main Technical Imperative of Software Development - complexity management

- Perfect code

Redux offers to manage complexity with predictable state changes. Predictability is achieved through three fundamental principles :



Was Redux able to overcome the increased complexity and was there anything to fight with?


MVC does not scale


Redux is inspired by Flux , a solution from Facebook. The reason for the creation of Flux, as stated by Facebook developers ( video ), was the problem of the scalability of the MVC architectural pattern.


According to the description of Facebook, the connections of objects in large projects using MVC eventually become unpredictable:


  1. modelOne changes viewOne
  2. viewOne changes modelTwo during its change
  3. modelTwo changes modelThree during its change
  4. modelThree changes viewTwo and viewFour during its change

The problem of the unpredictability of changes in MVC is also written in Redux's motivation. The picture below illustrates how Facebook developers see this problem.



Flux, in contrast to the described MVC, offers a clear and coherent model:


  1. View spawns Action
  2. Action enters Dispatcher
  3. Dispatcher Updates Store
  4. Updated Store notifies View of a change.
  5. View redraws


In addition, using Flux, several Views can subscribe to the Stores of interest and be updated only when something changes in these Stores. This approach reduces the number of dependencies and simplifies development.



The MVC implementation from Facebook is completely different from the original MVC, which was widely distributed in the Smalltalk world. This difference is the main reason for the statement "MVC does not scale."


Back in the eighties


MVC is the primary approach to developing user interfaces in Smalltalk-80. Like Flux and Redux, MVC was created to reduce software complexity and speed development. I will give a brief description of the basic principles of the MVC approach, a more detailed overview can be found here and here .


Responsibilities of MVC entities:



And now that Facebook missed, realizing MVC - the connections between these entities:



Look at the image below. The arrows directed from Model to Controller and View are not attempts to change their state, but alerts about changes in Model.



The original MVC is completely different from the Facebook implementation, in which the View can change the Model set, the Model can change the View set, and the Controller does not form a close one-to-one relationship with the View. Moreover, Flux is MVC, in which Dispatcher and Store play the role of Model, and Action is sent instead of calling methods.


React through MVC


Let's look at the code for a simple React component:


class ExampleButton extends React.Component {
  render() { return (
    <button onClick={() => console.log("clicked!")}>
        Click Me!
    </button>
  ); }
}

Controller'a MVC:


Controller , , View Model

ontroller View

, Controller View ? :


onClick={() => console.log("clicked!")}

Controller, . JavaScript , . React- View, View-Controller.


React, Model. React- Model .


MVC


React-, BaseView, props Model:


// src/Base/BaseView.tsx
import * as React from "react";
import BaseModel from "./BaseModel";

export default class <Model extends BaseModel, Props> extends React.Component<Props & {model: Model}, {}> {
    protected model: Model;

    constructor(props: any) {
        super(props);
        this.model = props.model
    }

    componentWillMount() { this.model.subscribe(this); }

    componentWillUnmount() { this.model.unsubscribe(this); }
}

state , . View this.forceUpdate(), . , , , .


BaseModel, , , :


// src/Base/BaseModel.ts
export default class {
    protected views: React.Component[] = [];

    subscribe(view: React.Component) {
        this.views.push(view);
        view.forceUpdate();
    }

    unsubscribe(view: React.Component) {
        this.views = this.views.filter((item: React.Component) => item !== view);
    }

    protected updateViews() {
        this.views.forEach((view: React.Component) => view.forceUpdate())
    }
}

TodoMVC , Github.


TodoMVC , . : " ", " ", " ". . :


// src/TodoList/TodoListModel.ts
import BaseModel from "../Base/BaseModel";
import TodoItemModel from "../TodoItem/TodoItemModel";

export default class extends BaseModel {
    private allItems: TodoItemModel[] = [];
    private mode: string = "all";

    constructor(items: string[]) {
        super();
        items.forEach((text: string) => this.addTodo(text));
    }

    addTodo(text: string) {
        this.allItems.push(new TodoItemModel(this.allItems.length, text, this));
        this.updateViews();
    }

    removeTodo(todo: TodoItemModel) {
        this.allItems = this.allItems.filter((item: TodoItemModel) => item !== todo);
        this.updateViews();
    }

    todoUpdated() { this.updateViews(); }

    showAll() { this.mode = "all"; this.updateViews(); }

    showOnlyActive() { this.mode = "active"; this.updateViews(); }

    showOnlyCompleted() { this.mode = "completed"; this.updateViews(); }

    get shownItems() {
        if (this.mode === "active") { return this.onlyActiveItems; }
        if (this.mode === "completed") { return this.onlyCompletedItems; }
        return this.allItems; 
    }

    get onlyActiveItems() {
        return this.allItems.filter((item: TodoItemModel) => item.isActive());
    }

    get onlyCompletedItems() {
        return this.allItems.filter((item: TodoItemModel) => item.isCompleted());
    }
}

. , , . :


// src/TodoItem/TodoItemModel.ts
import BaseModel from "../Base/BaseModel";
import TodoListModel from "../TodoList/TodoListModel";

export default class extends BaseModel {
    private completed: boolean = false;
    private todoList?: TodoListModel;
    id: number;
    text: string = "";

    constructor(id: number, text: string, todoList?: TodoListModel) {
        super();
        this.id = id;
        this.text = text;
        this.todoList = todoList;
    }

    switchStatus() { 
        this.completed = !this.completed
        this.todoList ? this.todoList.todoUpdated() : this.updateViews();
    }

    isActive() { return !this.completed; }

    isCompleted() { return this.completed; }

    remove() { this.todoList && this.todoList.removeTodo(this) }
}

View, Model. View :


// src/TodoList/TodoListInputView.tsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoListModel from "./TodoListModel";

export default class extends BaseView<TodoListModel, {}> {
    render() { return (
        <input 
            type="text"
            className="new-todo" 
            placeholder="What needs to be done?"
            onKeyDown={(e: any) => {
                const enterPressed = e.which === 13;
                if (enterPressed) { 
                    this.model.addTodo(e.target.value);
                    e.target.value = "";
                }
            }}
        />
    ); }
}

View, , Controller (props onKeyDown) Model View, Model . props' , .


View TodoListModel, :


// src/TodoList/TodoListView.tsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoListModel from "./TodoListModel";
import TodoItemModel from "../TodoItem/TodoItemModel";
import TodoItemView from "../TodoItem/TodoItemView";

export default class extends BaseView<TodoListModel, {}> {
    render() { return (
        <ul className="todo-list">
            {this.model.shownItems.map((item: TodoItemModel) => <TodoItemView model={item} key={item.id}/>)}
        </ul>
    ); }
}

View , TodoItemModel:


// src/TodoItem/TodoItemView.jsx
import * as React from "react";
import BaseView from "../Base/BaseView";
import TodoItemModel from "./TodoItemModel";

export default class extends BaseView<TodoItemModel, {}> {
    render() { return (
        <li className={this.model.isCompleted() ? "completed" : ""}>
            <div className="view">
                <input
                    type="checkbox"
                    className="toggle"
                    checked={this.model.isCompleted()}
                    onChange={() => this.model.switchStatus()}
                />
                <label>{this.model.text}</label>
                <button className="destroy" onClick={() => this.model.remove()}/>
            </div>
        </li>
    ); }
}

TodoMVC . , 60 . : Model View, . props', . Container-.


Redux?


, Redux , , Redux . frontend- :



Redux , .


Redux , , , . Redux indirection , Presentation Components , Action' State, props. indirection' . , .


indirection' TodoMVC, Redux. State callback' onSave, ?


,
  1. hadleSave TodoItem props onSave TodoTextInput
  2. onSave Enter , props newTodo, onBlur
  3. hadleSave props deleteTodo, , props editTodo
  4. props' deleteTodo editTodo TodoItem MainSection
  5. MainSection props' deleteTodo editTodo TodoItem
  6. props' MainSection App bindActionCreator, action' src/actions/index.js, src/reducers/todos.js

, callback', props', 2 . , .


MVC, , . indirection' , .


Flux Redux MVC, , MVC. Redux , callback' props' , . frontend-, Flux Redux, . . Facebook , "" . frontend- Facebook, . , , MVC ?


UPD


view.setState({}) view.forceUpdate(). , kahi4.


')

Source: https://habr.com/ru/post/350850/


All Articles