Framework independent front-end
GoF, Pure Architecture, Perfect Code are the True Programmer’s Handbooks. But in the front-end world, many ideas from these books are not available. At least resemblance to the real world is very difficult to find. Maybe the modern front-end is ahead of time? Maybe “functional” programming and React have already proven their superiority ? In this article I want to give an example of a todo-list application that I tried to implement according to the principles and approaches described in classic books.
Framework dependency
The framework is the cornerstone of the modern front. In our company are React vs Angular vs Vue developers. I worked with each of these frameworks, and for a very long time I could not understand why I had to work with Vue from 3 years to repaint a button from red to purple? Why do I need to know how to inherit on prototypes, or the principle of the event loop, to move the same button from the left corner to the right? The answer is simple – we write library-bound applications.
Why do companies with long experience working with React? Yes, because the application is highly dependent on the features of this React itself, and in order not to break anything when repainting the button, you should smash your head over how change detection, rendering of the component tree works inside React, and how it is tied to the task of repainting the button. (I agree, these are all special cases … And in your company, are you ready to take a specialist without experience working with the framework?)
For the world of the front-end, these theses are empty phrase, and a challenge to prove the opposite. Let’s look at the official React documentation, and see an example of a simple todo-list application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
class TodoApp extends React.Component { constructor(props) { super(props); this.state = { items: [], text: '' }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } render() { return ( <div> <h3> TodoList </h3> <TodoList items={this.state.items} /> <form onSubmit={this.handleSubmit}> <label htmlFor="new-todo"> What to do? </label> <input id="new-todo" onChange={this.handleChange} value={this.state.text} /> <button> add #{this.state.items.length + 1} </button> </form> </div> ); } handleChange(e) { this.setState({ text: e.target.value }); } handleSubmit(e) { e.preventDefault(); if (!this.state.text.length) { return; } const newItem = { text: this.state.text, id: Date.now() }; this.setState(state => ({ items: state.items.concat(newItem), text: '' })); } } class TodoList extends React.Component { render() { return ( <ul> {this.props.items.map(item => ( <li key={item.id}>{item.text}</li> ))} </ul> ); } } |
For a novice programmer (i.e. me a couple of years ago), this phrase automatically generates the output: “Here is an ideal example of a todo-list application”. But who stores the state in the component ?! For this there is a state management library (https://redux.js.org/basics/example/).
Yes, so the application has become much more understandable and simpler (no). Can we try to make the dependencies in the right direction?
Independent decision
Let’s look at the problem of todo-list not as front-end, and forget that we need to make HTML (“web is a detail”). We won’t be able to check the results with our eyes, so we’ll have to write tests (as Uncle Bob says, “TDD can be applied then”). And what is the task? What is todo-list? We are trying to write.
1 |
import { Todo } from './Todo'; describe('Todo', () => { let todo: Todo; beforeEach(() => { todo = new Todo('description'); }); it('+getItems() should returns Todo[]', () => { expect(todo.getTitle()).toBe('description'); }); it('+isCompleted() should returns completion flag', () => { expect(todo.isCompleted()).toBe(false); }); it('+toggleCompletion() should invert completion flag', () => { todo.toggleCompletion(); expect(todo.isCompleted()).toBe(true); }); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
export class Todo { private completed: boolean = false; constructor(private description: string) {} getTitle(): string { return this.description; } isCompleted(): boolean { return this.completed; } toggleCompletion(): void { this.completed = !this.completed; } } import { Todo } from './Todo'; export class TodoList { private items: Todo[] = []; getItems(): Todo[] { return this.items; } getCompletedItems(): Todo[] { return this.items.filter((todo) => todo.isCompleted()); } getUncompletedItems(): Todo[] { return this.items.filter((todo) => !todo.isCompleted()); } add(description: string): void { this.items.push(new Todo(description)); } } |
We get two simple classes with informative interfaces. Is that all? Tests pass. Now pick up the React.
1 |
import React from 'react'; import { TodoList } from './core/TodoList'; export class App extends React.Component { todoList: TodoList = this.createTodoList(); render(): any { return ( <React.Fragment> <header> <h1>Todo List App</h1> </header> <main> <TodoListCmp todoList={this.todoList}></TodoListCmp> <AddTodoCmp todoList={this.todoList}></AddTodoCmp> </main> </React.Fragment> ); } private createTodoList(): TodoList { const todoList = new TodoList(); todoList.add('Initial created Todo'); return todoList; } } export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => { return ( <div> <h2>What to do?</h2> <ul> {todoList.getItems().map((todo) => ( <li key={todo.getTitle()}>{todo.getTitle()}</li> ))} </ul> </div> ); }; export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => { return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>; }; |
And make sure that … Adding an item does not work. Hmm … Now it’s clear why everything needs to be written in state – so that the React component redraws after learning about the changes. But is this a reason to violate all possible principles and put logic in the view component? A little patience and courage. To solve the problem, calling forceUpdate () in an infinite loop or the Observer pattern is perfect.
I like the RxJs library, but I will not connect it, but just copy its API necessary for our task.
In my opinion, nothing too complicated. Add a test (notification of changes ).
1 |
it('+TodoList.prototype.add() should emit changes', async () => { const spy = jasmine.createSpy(); todoList.changes.subscribe(spy); todoList.add('description'); await delay(); expect(spy).toHaveBeenCalled(); }); |
Let’s think for a moment, does a change in a Todo element affect the state of a TodoList? Affects – the getCompletedItems / getUncompletedItems methods must return a different set of elements. Maybe it’s worth moving the toggleCompletion to the TodoList class? It’s a bad idea – with this approach, we’ll have to inflate TodoList for every feature concerning a new Todo-element (we will return to this later). But how to learn about the changes, again the Observer? To make things simpler, let the Todo-element itself communicate changes through a callback.
The full version of the program looks like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
import React from 'react'; import { Observable, Subject } from 'src/utils/Observable'; import { generateId } from 'src/utils/generateId'; export class Todo { private completed: boolean = false; id: string = generateId(); constructor(private description: string, private onCompletionToggle?: (todo: Todo) => void) {} getTitle(): string { return this.description; } isCompleted(): boolean { return this.completed; } toggleCompletion(): void { this.completed = !this.completed; this.onCompletionToggle?.(this); } } export class TodoList { private items: Todo[] = []; private changesSubject = new Subject(); readonly changes: Observable = this.changesSubject.asObservable(); getItems(): Todo[] { return this.items; } getCompletedItems(): Todo[] { return this.items.filter((todo) => todo.isCompleted()); } getUncompletedItems(): Todo[] { return this.items.filter((todo) => !todo.isCompleted()); } add(description: string): void { this.items.push(new Todo(description, () => this.changesSubject.next({}))); this.changesSubject.next({}); } } export class App extends React.Component { todoList: TodoList = this.createTodoList(); render(): any { return ( <React.Fragment> <header> <h1>Todo List App</h1> </header> <main> <TodoListCmp todoList={this.todoList}></TodoListCmp> <AddTodoCmp todoList={this.todoList}></AddTodoCmp> </main> </React.Fragment> ); } componentDidMount(): void { this.todoList.changes.subscribe(() => this.forceUpdate()); } private createTodoList(): TodoList { const todoList = new TodoList(); todoList.add('Initial created Todo'); return todoList; } } export const TodoListCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => { return ( <div> <h2>What to do?</h2> <ul> {todoList.getUncompletedItems().map((todo) => ( <TodoCmp key={todo.id} todo={todo}></TodoCmp> ))} {todoList.getCompletedItems().map((todo) => ( <TodoCmp key={todo.id} todo={todo}></TodoCmp> ))} </ul> </div> ); }; export const TodoCmp: React.FC<{ todo: Todo }> = ({ todo }) => ( <li style={{ textDecoration: todo.isCompleted() ? 'line-through' : '' }} onClick={() => todo.toggleCompletion()} > {todo.getTitle()} </li> ); export const AddTodoCmp: React.FC<{ todoList: TodoList }> = ({ todoList }) => { return <button onClick={() => todoList.add(`Todo ${todoList.getItems().length}`)}>Add</button>; }; |
This seems to be what the todo-list application independent of the framework should look like. The only limitation is PL. You can implement the display for the console, or use Angular.
Perhaps the current version of the application is not complex enough to make sure that an independent approach is working and to demonstrate its strengths. Therefore, we’ll connect our imagination to simulate a more or less plausible todo-list development scenario.
Edits from the customer
The main nightmare of most projects is changing requirements. You know that edits cannot be avoided, and you know that this is normal. But how do you prepare for future changes?
Special Todo Elements
One of the key features of OOP is the ability to solve the problem through the introduction of new types. Perhaps this is the most powerful OOP technique that can single-handedly pull out a complex and cumbersome program. For example, I do not know what is required of a Todo element. It may be necessary to be able to change its name, it may be necessary to add additional attributes, it may be possible to change this element through direct access to the SpaceX server … But I’m sure that the requirements will change, and I will need different types of Todo.
1 2 3 4 5 6 |
export class EditableTodo extends Todo { changeTitle(title: string): void { this.title = title; this.onChange?.(this); } } |
It seems that to display a special type, we will need to change the view of components as well. In practice, I met (and wrote) components in which a million different conditions turn a div block from a giraffe into a machine gun. To avoid this problem, you can create a hoc component with a huge switch-case list. Or apply the Visitor pattern and dual dispatch, and let the Todo element decide for itself what type of component to draw.
The dual dispatch option is especially useful when the same type of element has different views. You can change them by substituting different TodoRenderers into the render method.
Now we are ready. Fear of new demands for “special” Todo elements has disappeared. I think the developers themselves could take the initiative, and offer a couple of features that require the introduction of new types, which are now added by writing new code and minimal change to the existing one.
1 |
export class Todo { id: string = ''; constructor( protected title: string, private completed: boolean = false, protected onChange?: (todo: Todo) => void, ) {} getTitle(): string { return this.title; } isCompleted(): boolean { return this.completed; } toggleCompletion(): void { this.completed = !this.completed; this.onChange?.(this); } render(renderer: TodoRenderer): any { return renderer.renderSimpleTodo(this); } } export class EditableTodo extends Todo { changeTitle(title: string): void { this.title = title; this.onChange?.(this); } render(renderer: TodoRenderer): any { return renderer.renderEditableTodo(this); } } export class TodoRenderer { renderSimpleTodo(todo: Todo): any { return <SimpleTodoCmp todo={todo}></SimpleTodoCmp>; } renderFixedTodo(todo: Todo): any { return <FixedTodoCmp todo={todo}></FixedTodoCmp>; } renderEditableTodo(todo: EditableTodo): any { return <EditableTodoCmp todo={todo}></EditableTodoCmp>; } } |
Saving data to the server
What kind of application is without interacting with the server? Of course, you need to be able to save our list via HTTP – another new requirement. We are trying to solve the problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
export interface TodoListApi { getItems(): Promise<TodoParams[]>; save(todoParamsList: TodoParams[]): Promise<void>; } export class AppTodoList implements TodoList { private todoFactory = new TodoFactory(); private changesSubject = new Subject(); changes: Observable = this.changesSubject.asObservable(); private state: TodoList = new TodoListImp(); private subscription: Subscription = this.state.changes.subscribe(() => this.onStateChanges()); private synchronizedTodoParamsList: TodoParams[] = []; constructor(private api: TodoListApi) {} async resolve(): Promise<void> { const todoParamsList = await this.api.getItems(); this.updateState(todoParamsList); } private updateState(todoParamsList: TodoParams[]): void { const todoList = new TodoListImp(todoParamsList); this.state = todoList; this.subscription.unsubscribe(); this.subscription = todoList.changes.subscribe(() => this.onStateChanges()); this.synchronizedTodoParamsList = todoParamsList; this.changesSubject.next({}); } private async onStateChanges(): Promise<void> { this.changesSubject.next({}); try { const params = this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo)); await this.api.save(params); this.synchronizedTodoParamsList = params; } catch { this.updateState(this.synchronizedTodoParamsList); } } destroy(): void { this.subscription.unsubscribe(); } getItems(): Todo[] { return this.state.getItems(); } getCompletedItems(): Todo[] { return this.state.getCompletedItems(); } getUncompletedItems(): Todo[] { return this.state.getUncompletedItems(); } add(todoParams: TodoParams): void { this.state.add(todoParams); } } |
We do not know how the application should behave. Wait for the save to succeed and display the changes? Allow changes to be displayed, and in case of error, roll back to synchronized state? Or ignore save errors altogether? Most likely, the customer also does not know this. Therefore, changes to requirements are inevitable, but they should affect only one class responsible for saving. And on the way is the next edit.
History changes
“Need the ability to cancel / redo actions” …
A wave of new edits seems to take us by surprise. But in no case, you can not sacrifice tests. Now it is completely unclear which inheritance hierarchy is better suited, and whether inheritance is generally suitable. Therefore, nothing bad will happen if we simply supplement our dirty class (we will consider this the implementation details), sacrificing the principle of sole responsibility.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
import { TodoParams } from 'src/core/TodoFactory'; import { delay } from 'src/utils/delay'; import { TodoListHistory } from './TodoListHistory'; describe('TodoListHistory', () => { let history: TodoListHistory; beforeEach(() => { history = new TodoListHistory(); }); it('+getState() should returns TodoParams[]', () => { expect(history.getState()).toEqual([]); }); it('+setState() should rewrite current state', () => { const newState = [{ title: '' }] as TodoParams[]; history.setState(newState); expect(history.getState()).toBe(newState); }); it('+hasPrev() should returns false on init', () => { expect(history.hasPrev()).toBe(false); }); it('+hasPrev() should returns true after setState()', () => { history.setState([]); expect(history.hasPrev()).toBe(true); }); it('+switchToPrev() should switch on prev state', () => { const prevState = [{ title: '' }] as TodoParams[]; history.setState(prevState); history.setState([]); history.switchToPrev(); expect(history.getState()).toBe(prevState); }); it('+hasPrev() should returns false after switch to first', () => { history.setState([]); history.switchToPrev(); expect(history.hasPrev()).toBe(false); }); it('+hasNext() should returns false on init', () => { expect(history.hasNext()).toBe(false); }); it('+hasNext() should returns true after switchToPrev()', () => { history.setState([]); history.switchToPrev(); expect(history.hasNext()).toBe(true); }); it('+switchToNext() should switch on next state', () => { const prevState = [{ title: '' }] as TodoParams[]; history.setState([]); history.setState(prevState); history.switchToPrev(); history.switchToNext(); expect(history.getState()).toBe(prevState); }); it('+hasNext() should returns false after switchToNext()', () => { history.setState([]); history.switchToPrev(); history.switchToNext(); expect(history.hasNext()).toBe(false); }); it('+hasNext() should returns false after setState()', () => { history.setState([]); history.switchToPrev(); history.setState([]); expect(history.hasNext()).toBe(false); }); it('+switchToPrev() should switch on prev state after setState()', () => { const prevState = [{ title: '' }] as TodoParams[]; history.setState(prevState); history.setState([]); history.switchToPrev(); history.setState([]); history.switchToPrev(); expect(history.getState()).toBe(prevState); }); it('+setState() should not emit changes', async () => { const spy = jasmine.createSpy(); history.changes.subscribe(spy); history.setState([]); await delay(); expect(spy).not.toHaveBeenCalled(); }); it('+switchToPrev() should emit changes', async () => { history.setState([]); await delay(); const spy = jasmine.createSpy(); history.changes.subscribe(spy); history.switchToPrev(); await delay(); expect(spy).toHaveBeenCalled(); }); it('+switchToPrev() should emit changes', async () => { history.setState([]); history.switchToPrev(); await delay(); const spy = jasmine.createSpy(); history.changes.subscribe(spy); history.switchToNext(); await delay(); expect(spy).toHaveBeenCalled(); }); it('+reset() should reset history and apply initial state', async () => { history.setState([]); expect(history.hasPrev()).toBe(true); const initState = [{ title: '' }] as TodoParams[]; history.reset(initState); expect(history.hasPrev()).toBe(false); expect(history.getState()).toBe(initState); }); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
import { TodoParams } from 'src/core/TodoFactory'; import { Observable, Subject } from 'src/utils/Observable'; export class TodoListHistory { private changesSubject = new Subject(); private history: TodoParams[][] = [this.state]; changes: Observable = this.changesSubject.asObservable(); constructor(private state: TodoParams[] = []) {} reset(state: TodoParams[]): void { this.state = state; this.history = [this.state]; } getState(): TodoParams[] { return this.state; } setState(state: TodoParams[]): void { this.deleteHistoryAfterCurrentState(); this.state = state; this.history.push(state); } private nextState(state: TodoParams[]): void { this.state = state; this.changesSubject.next({}); } private deleteHistoryAfterCurrentState(): void { this.history = this.history.slice(0, this.getCurrentStateIndex() + 1); } hasPrev(): boolean { return this.getCurrentStateIndex() > 0; } hasNext(): boolean { return this.getCurrentStateIndex() < this.history.length - 1; } switchToPrev(): void { const prevStateIndex = Math.max(this.getCurrentStateIndex() - 1, 0); this.nextState(this.history[prevStateIndex]); } switchToNext(): void { const nextStateIndex = Math.min(this.getCurrentStateIndex() + 1, this.history.length - 1); this.nextState(this.history[nextStateIndex]); } private getCurrentStateIndex(): number { return this.history.indexOf(this.state); } } import { Observable, Subject, Subscription } from 'src/utils/Observable'; import { Todo } from '../core/Todo'; import { TodoFactory, TodoParams } from '../core/TodoFactory'; import { TodoList, TodoListImp } from '../core/TodoList'; import { TodoListApi } from './TodoListApi'; import { HistoryControl, TodoListHistory } from './TodoListHistory'; export class AppTodoList implements TodoList { private readonly todoFactory = new TodoFactory(); private readonly history: TodoListHistory = new TodoListHistory(); private changesSubject = new Subject(); readonly changes: Observable = this.changesSubject.asObservable(); private state: TodoList = new TodoListImp(); private stateSubscription: Subscription = this.state.changes.subscribe(() => this.onStateChanges(), ); private historySubscription = this.history.changes.subscribe(() => this.onHistoryChanges()); constructor(private api: TodoListApi) {} private onStateChanges(): void { const params = this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo)); this.history.setState(params); this.api.save(params).catch(() => {}); this.changesSubject.next({}); } private onHistoryChanges(): void { const params = this.history.getState(); this.updateStateTodoList(params); this.api.save(params).catch(() => {}); } private updateStateTodoList(todoParamsList: TodoParams[]): void { const todoList = new TodoListImp(todoParamsList); this.state = todoList; this.stateSubscription.unsubscribe(); this.stateSubscription = this.state.changes.subscribe(() => this.onStateChanges()); this.changesSubject.next({}); } async resolve(): Promise<void> { const todoParamsList = await this.api.getItems(); this.history.reset(todoParamsList); this.updateStateTodoList(todoParamsList); } destroy(): void { this.stateSubscription.unsubscribe(); this.historySubscription.unsubscribe(); } getHistory(): HistoryControl<TodoParams[]> { return this.history; } getItems(): Todo[] { return this.state.getItems(); } getCompletedItems(): Todo[] { return this.state.getCompletedItems(); } getUncompletedItems(): Todo[] { return this.state.getUncompletedItems(); } add(todoParams: TodoParams): void { this.state.add(todoParams); } } |
Since everything is approximately unambiguous with the change history, we will separate the management of the todo-list history into the base class.
1 |
import { Todo } from 'src/core/Todo'; import { Observable, Subject, Subscription } from 'src/utils/Observable'; import { TodoFactory, TodoParams } from '../core/TodoFactory'; import { TodoList, TodoListImp } from '../core/TodoList'; import { HistoryControl, HistoryState } from './HistoryState'; export class HistoricalTodoList implements TodoList, HistoryControl { protected readonly todoFactory = new TodoFactory(); protected readonly history = new HistoryState<TodoParams[]>([]); private changesSubject: Subject = new Subject(); readonly changes: Observable = this.changesSubject.asObservable(); private state: TodoList = new TodoListImp(); private stateSubscription: Subscription = this.state.changes.subscribe(() => this.onStateChanged(this.getSerializedState()), ); constructor() {} protected onStateChanged(params: TodoParams[]): void { this.history.addState(params); this.changesSubject.next({}); } protected onHistorySwitched(): void { this.updateState(this.history.getState()); } protected updateState(todoParamsList: TodoParams[]): void { this.state = new TodoListImp(todoParamsList); this.updateStateSubscription(); this.changesSubject.next({}); } private updateStateSubscription(): void { this.stateSubscription.unsubscribe(); this.stateSubscription = this.state.changes.subscribe(() => this.onStateChanged(this.getSerializedState()), ); } private getSerializedState(): TodoParams[] { return this.state.getItems().map((todo) => this.todoFactory.serializeTodo(todo)); } destroy(): void { this.stateSubscription.unsubscribe(); } getItems(): Todo[] { return this.state.getItems(); } getCompletedItems(): Todo[] { return this.state.getCompletedItems(); } getUncompletedItems(): Todo[] { return this.state.getUncompletedItems(); } add(todoParams: TodoParams): void { this.state.add(todoParams); } canUndo(): boolean { return this.history.hasPrev(); } canRedo(): boolean { return this.history.hasNext(); } undo(): void { this.history.switchToPrev(); this.onHistorySwitched(); } redo(): void { this.history.switchToNext(); this.onHistorySwitched(); } } import { TodoParams } from 'src/core/TodoFactory'; import { HistoricalTodoList } from './HistoricalTodoList'; import { TodoListApi } from './TodoListApi'; export class ResolvableTodoList extends HistoricalTodoList { constructor(private api: TodoListApi) { super(); } async resolve(): Promise<void> { const todoParamsList = await this.api.getItems(); this.history.reset(todoParamsList); this.updateState(todoParamsList); } protected onStateChanged(params: TodoParams[]): void { super.onStateChanged(params); this.api.save(params).catch(() => this.undo()); } protected onHistorySwitched(): void { super.onHistorySwitched(); this.api.save(this.history.getState()).catch(() => {}); } } |
The solution to the saving problem came by itself. Now we can not worry about which saving strategy the customer ultimately chooses. You can provide him with all 3 options to choose from, expanding the base class.
Summary
It seems that from our todo-list we’ve got a small prototype of Google Keep.
What fundamentally differs this example from most front-end applications? We did not depend on libraries, therefore, a person who has never worked with React can understand this application. Our decisions were aimed only at obtaining the result, without distractions on the details of the framework, so the code more or less reflects the problem being solved. We managed to make it easy to add new types of Todo elements, and we are ready to change the conservation strategy.
What difficulties did we encounter? We solved the problem of updating the view using the Observer pattern without reference to the framework. As it turned out, the application of this pattern was still required to solve the main problem (even if we did not need to draw HTML). Therefore, we did not incur costs by abandoning the “services” of the change detection system built into the framework.
I would like to emphasize that writing tests was not any difficulty. Testing simple independent objects with an informative interface is a pleasure. The complexity of the code depended only on the task itself and my skills (or curvature).
What about the level of developer who would handle this task? Could Junior React Developer write such a solution? “Programming is more like a craft,” so without the practice of using OOP and patterns, I think it would be difficult. But you and your company decide what you invest in. Do you practice OOP or understand the intricacies of the next framework? I only again became convinced of the relevance of the literary works of experienced programmers, and showed how you can start using the advice of the classics at full capacity at the front-end.
Thanks for reading!
Related Posts
Leave a Reply Cancel reply
Service
Categories
- DEVELOPMENT (103)
- DEVOPS (53)
- FRAMEWORKS (26)
- IT (25)
- QA (14)
- SECURITY (13)
- SOFTWARE (13)
- UI/UX (6)
- Uncategorized (8)