Jekyll2022-05-20T06:49:51+00:00https://georgebyte.com/feed.xmlGeorge ByteTech lead and full stack web engineer deeply in love with front-end development.Scalable Angular app architecture2019-03-20T00:00:00+00:002019-03-20T00:00:00+00:00https://georgebyte.com/scalable-angular-app-architecture<p class="excerpt">
This article is a collection of my knowledge about building robust and scalable front-end applications in Angular. First part presents the concepts which I've found are worth adopting when developing front-end applications. In the second part I showcase an Angular app architecture built on top of these concepts.
</p>
<p class="note">
Note: All code examples used in this article are simplified snippets of code from the <a href="https://github.com/georgebyte/coffee-election-ng-app-example" target="_blank">Coffee Election app</a>. Coffee Election app is an Angular app showcasing the scalable Angular app architecture described below. It lets its users vote for their favorite type of coffee and displays voting results. To see actual, non-simplified implementation, click on the file name above code snippets.
</p>
<h2 id="contents-">Contents <!-- omit in toc --></h2>
<ul>
<li><a href="#1-main-ideas-and-concepts">1. Main ideas and concepts</a>
<ul>
<li><a href="#11-state-management-with-observable-store-services">1.1 State management with observable store services</a></li>
<li><a href="#12-component-based-architecture">1.2 Component based architecture</a>
<ul>
<li><a href="#121-presentational-components">1.2.1 Presentational components</a></li>
<li><a href="#122-smart-container-components">1.2.2 Smart container components</a></li>
</ul>
</li>
<li><a href="#13-one-way-data-flow">1.3 One-way data flow</a></li>
<li><a href="#14-communication-with-external-systems">1.4 Communication with external systems</a></li>
</ul>
</li>
<li><a href="#2-modules">2. Modules</a>
<ul>
<li><a href="#21-apps-root">2.1 App’s root</a></li>
<li><a href="#22-core-module">2.2 Core module</a></li>
<li><a href="#23-feature-modules">2.3 Feature modules</a></li>
<li><a href="#24-shared-module">2.4 Shared module</a></li>
<li><a href="#25-layout-module">2.5 Layout module</a></li>
<li><a href="#26-views-module">2.6 Views module</a></li>
<li><a href="#27-styles">2.7 Styles</a></li>
</ul>
</li>
<li><a href="#3-testing">3. Testing</a></li>
<li><a href="#4-conclusion">4. Conclusion</a></li>
</ul>
<h2 id="1-main-ideas-and-concepts">1. Main ideas and concepts</h2>
<p>This section will present the patterns and main ideas used to create a robust and scalable front-end app architecture. These concepts are in my opinion current state of the art when it comes to front-end development and I use them daily when building production ready front-end apps. Although I’ll present them in the context of Angular framework, I suggest you consider adopting their slightly modified version even when developing apps using other frameworks.</p>
<h3 id="11-state-management-with-observable-store-services">1.1 State management with observable store services</h3>
<p>Effective state management is crucial in larger front-end applications. This scalable Angular app architecture was designed with observable store services as its main way of managing state. Observable stores are a state management solution implemented using RxJS to mimic Redux architecture. I described them in depth in my previous article about <a href="/state-management-in-angular-with-observable-store-services/" target="_blank">State management in Angular with observable store services</a>. I recommend that you check it out before you continue reading this article.</p>
<h3 id="12-component-based-architecture">1.2 Component based architecture</h3>
<p>Component based architecture has gained a lot of popularity in front-end development over the past few years. It’s a pattern that fits nicely in the context of developing front-end applications and enables developers to write maintainable and extensible code.</p>
<p>The scalable Angular app architecture described in this article is strongly rooted in component based architecture. The purpose of many ideas written bellow is to enhance components’ reusability which in turn makes apps easier (and way more fun) to understand and extend.</p>
<p>In my opinion, the most important part of creating truly reusable components is to separate them into <strong>containers and presentational components</strong>.</p>
<h4 id="121-presentational-components">1.2.1 Presentational components</h4>
<p>In the <a href="/state-management-in-angular-with-observable-store-services/" target="_blank">previous article about observable store services</a> we’ve learned how to store app’s state in observable stores. To make an app useful though we would want to present this state to the users and create an interface for them to interact with the state. This is where presentational components come into play.</p>
<p>Presentational components are responsible for rendering the current state and providing an interface that makes it possible for the user to interact with the app. They define how the rendered state should look like in their templates and style definitions. They also setup event listeners to handle the interaction part of their responsibilities.</p>
<p>Let’s see how the theory of presentational components looks like in practice. We’ll first have a look at a simplified template of a coffee candidate that the users can vote for.</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/components/coffee-candidate/coffee-candidate.component.html" target="_blank">coffee-candidate.component.html</a>
</span></p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-coffee-candidate__name"</span><span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-coffee-candidate__label"</span><span class="nt">></span>Name<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-coffee-candidate__value"</span><span class="nt">></span>{{candidate.name}}<span class="nt"></div></span>
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-coffee-candidate__action"</span><span class="nt">></span>
<span class="nt"><button</span>
<span class="na">class=</span><span class="s">"ce-button ce-button--primary ce-coffee-candidate__action-button"</span>
<span class="na">(click)=</span><span class="s">"onUserAction.emit(UserAction.AddVote)"</span>
<span class="nt">></span>
Add vote
<span class="nt"></button></span>
<span class="nt"></div></span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>The template above renders <code class="language-plaintext highlighter-rouge">candidate</code>’s name and sets up a <code class="language-plaintext highlighter-rouge">click</code> event handler on “Add vote” button that triggers <code class="language-plaintext highlighter-rouge">onUserAction.emit(UserAction.AddVote)</code>. The properties and methods used are defined in component class:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/components/coffee-candidate/coffee-candidate.component.ts" target="_blank">coffee-candidate.component.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">selector</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ce-coffee-candidate</span><span class="dl">'</span><span class="p">,</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="dl">'</span><span class="s1">./coffee-candidate.component.html</span><span class="dl">'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">./coffee-candidate.component.scss</span><span class="dl">'</span><span class="p">],</span>
<span class="na">changeDetection</span><span class="p">:</span> <span class="nx">ChangeDetectionStrategy</span><span class="p">.</span><span class="nx">OnPush</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeCandidateComponent</span> <span class="p">{</span>
<span class="p">@</span><span class="nd">Input</span><span class="p">()</span>
<span class="nx">candidate</span><span class="p">:</span> <span class="nx">Candidate</span><span class="p">;</span>
<span class="p">@</span><span class="nd">Output</span><span class="p">()</span>
<span class="nx">onUserAction</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventEmitter</span><span class="o"><</span><span class="nx">UserAction</span><span class="o">></span><span class="p">();</span>
<span class="nx">UserAction</span> <span class="o">=</span> <span class="nx">UserAction</span><span class="p">;</span> <span class="c1">// Constants exposed to component's template</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>As you can see the component class is pretty simple. There is basically only one <code class="language-plaintext highlighter-rouge">Input</code> and one <code class="language-plaintext highlighter-rouge">Output</code> defined. And that’s the point. <code class="language-plaintext highlighter-rouge">CoffeeCandidateComponent</code> is only concerned with the presentation of state (passed in via inputs) and reacting to user actions by emitting events (via outputs).</p>
<p>In many cases presentational components need additional methods and properties in order to create more complicated user interfaces. Some examples would be:</p>
<ul>
<li>a <code class="language-plaintext highlighter-rouge">boolean</code> property <code class="language-plaintext highlighter-rouge">isSectionVisible</code> used to show/hide a certain section of component’s UI,</li>
<li>methods to compute (from input data) some additional properties rendered in the UI,</li>
<li>event handlers for more complex user interactions,</li>
<li>methods to throttle or debounce user input etc.</li>
</ul>
<p>Logic needed to implement these functionalities can be quite complex. But no matter how complex it is, it should always be concerned with just the presentation of app’s state and capturing of user actions. There should be <strong>no business logic, no direct app’s state updates, no API calls etc.</strong> in presentational components. This should be handled by observable stores or other services.</p>
<p>Presentational components can also include other components in their templates in order to keep their purpose as focused as possible. These additional components can be defined in presentational component’s template directly or <a href="https://medium.com/claritydesignsystem/ng-content-the-hidden-docs-96a29d70d11b" target="_blank">projected from parent components</a> into a <code class="language-plaintext highlighter-rouge"><ng-content></code> tag.</p>
<p>What one gets by implementing this pattern of presentational components is a clear separation of concerns. <strong>Presentational components are decoupled from the app’s business logic and have no clue about app’s state structure.</strong> They <strong>define the rules (an interface)</strong> of how to communicate with them via typed inputs and outputs. And this makes presentational components truly <strong>reusable</strong>.</p>
<p>Good, we’ve got presentational components covered. Let’s continue and explore the other type of components we need in order to create an actual app. Presentational components are decoupled from business logic and app’s state structure, but we still need to somehow connect their inputs and outputs correctly to that business logic and app’s state. And that’s the role of smart container components.</p>
<h4 id="122-smart-container-components">1.2.2 Smart container components</h4>
<p>Smart container components are components that act as a “glue” which <strong>binds observable stores and other business logic with presentational components</strong> in a loosely coupled way. They are “smart” because, in order achieve this, they must know how app’s state is structured, which stores contain the state required, which store’s method to call when an output callback is triggered by a presentational component etc. Because of that, container components are much more specific to app’s features and their reusability is lower. But that’s fine - some parts of the app must be smart so that the app can do smart things.</p>
<p>A container component class may look something like this (please refer to my previous article about <a href="/state-management-in-angular-with-observable-store-services/" target="_blank">observable store services</a> if any of the code examples bellow don’t make sense to you):</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/views/coffee-list/coffee-list.view.ts" target="_blank">coffee-list.view.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="na">templateUrl</span><span class="p">:</span> <span class="dl">'</span><span class="s1">./coffee-list.view.html</span><span class="dl">'</span><span class="p">,</span>
<span class="na">styleUrls</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">./coffee-list.view.scss</span><span class="dl">'</span><span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">CoffeeListStore</span><span class="p">,</span> <span class="nx">CoffeeListEndpoint</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListView</span> <span class="k">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="k">public</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">CoffeeListStore</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">ngOnInit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">init</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Nothing too complicated. The most interesting parts are:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">providers: [CoffeeListStore, CoffeeListEndpoint]</code> which defines the services we’ll need in this component and it’s subcomponents,</li>
<li><code class="language-plaintext highlighter-rouge">constructor(public store: CoffeeListStore) {}</code> which creates a new instance of the observable store,</li>
<li><code class="language-plaintext highlighter-rouge">this.store.init()</code> which initializes the observable store.</li>
</ul>
<p>The template of a container component is much more interesting in my opinion:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/views/coffee-list/coffee-list.view.html" target="_blank">coffee-list.view.html</a>
</span></p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="code"><pre><span class="nt"><ng-container</span> <span class="na">*ngIf=</span><span class="s">"{state$: store.state$ | async} as subs"</span><span class="nt">></span>
<span class="nt"><div</span>
<span class="na">class=</span><span class="s">"ce-panel ce-coffee-list-view__list-item"</span>
<span class="na">*ngIf=</span><span class="s">"subs.state$.requests.listCandidates.inProgress"</span>
<span class="nt">></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-loader ce-loader--takeover"</span><span class="nt">></div></span>
<span class="nt"></div></span>
<span class="nt"><ng-container</span> <span class="na">*ngIf=</span><span class="s">"!subs.state$.requests.listCandidates.inProgress"</span><span class="nt">></span>
<span class="nt"><div</span>
<span class="na">class=</span><span class="s">"ce-panel ce-coffee-list-view__list-item"</span>
<span class="na">*ngFor=</span><span class="s">"let candidate of subs.state$.candidateList.candidates"</span>
<span class="nt">></span>
<span class="nt"><ce-coffee-candidate</span>
<span class="na">class=</span><span class="s">"ce-coffee-candidate ce-coffee-list-view__candidate"</span>
<span class="na">[candidate]=</span><span class="s">"candidate"</span>
<span class="na">(onUserAction)=</span><span class="s">"store.submitUserAction(candidate, $event)"</span>
<span class="nt">></ce-coffee-candidate></span>
<span class="nt"></div></span>
<span class="nt"></ng-container></span>
<span class="nt"></ng-container></span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>The example above shows a container component “in action”. First a <code class="language-plaintext highlighter-rouge">subs</code> object is created whose role is to store subscriptions to different observables stores. This is an optimization so that only one subscription per store is created in a template by <a href="https://angular.io/api/common/NgIf#storing-a-conditional-result-in-a-variable" target="_blank">storing a conditional result in a variable</a>. Otherwise, <code class="language-plaintext highlighter-rouge">async</code> pipe would create a new subscription for every template binding using <code class="language-plaintext highlighter-rouge">store.state$</code> observable.</p>
<p>The next interesting part is the inclusion of a presentational component (<code class="language-plaintext highlighter-rouge"><ce-coffee-candidate></code>). Notice how the container component wires correct inputs and outputs to <code class="language-plaintext highlighter-rouge">CoffeeCandidateComponent</code>. As I stated before, container component knows how <code class="language-plaintext highlighter-rouge">store</code>’s state is structured and which public methods of modifying the state exist in the store. It also knows what the interface exposed by <code class="language-plaintext highlighter-rouge">CoffeeCandidateComponent</code> looks like. And it knows how to connect the store to <code class="language-plaintext highlighter-rouge">CoffeeCandidateComponent</code> in order to add a voting feature to the app.</p>
<p>The best part is that the store doesn’t care who uses its state and in what way it is used. It is instead concerned with implementing the right business logic and state persistance. The store assumes a “smart” consumer knows how to use its exposed interface. Similarly <code class="language-plaintext highlighter-rouge">CoffeeCandidateComponent</code> presentational component isn’t concerned with where a <code class="language-plaintext highlighter-rouge">candidate</code> to render comes from or how to update the state when user votes for this coffee candidate.</p>
<p>This clear separation of concerns makes the app much easier to understand and extend with new features. And it enables data to “flow” in one direction through the app. I’ll explain this further in a minute.</p>
<p>There’s just one more thing left to explain in this section. You may have noticed the above container component is actually called a “view”. This isn’t a mistake.</p>
<p>A <strong>view</strong> is a special type of container component. It’s a smart container <strong>component which can be routed to</strong> by Angular <code class="language-plaintext highlighter-rouge">Router</code>. In other words, it’s a component included in the list of <code class="language-plaintext highlighter-rouge">Routes</code> with a path specified:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/coffee-list-routing.module.ts" target="_blank">coffee-list-routing.module.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="code"><pre><span class="kd">const</span> <span class="nx">routes</span><span class="p">:</span> <span class="nx">Routes</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">list</span><span class="dl">'</span><span class="p">,</span>
<span class="na">component</span><span class="p">:</span> <span class="nx">CoffeeListView</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">];</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span><span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forChild</span><span class="p">(</span><span class="nx">routes</span><span class="p">)],</span>
<span class="na">exports</span><span class="p">:</span> <span class="p">[</span><span class="nx">RouterModule</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListRoutingModule</span> <span class="p">{}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Views are quite similar to regular container components. The only difference is that views are responsible for <strong>synching query params’ state with state in app’s stores</strong>. An example will make this statement much clearer:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/views/coffee-list/coffee-list.view.ts" target="_blank">coffee-list.view.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="code"><pre><span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListView</span> <span class="k">implements</span> <span class="nx">OnInit</span><span class="p">,</span> <span class="nx">OnDestroy</span> <span class="p">{</span>
<span class="k">private</span> <span class="nx">ngUnsubscribe$</span><span class="p">:</span> <span class="nx">Subject</span><span class="o"><</span><span class="kc">undefined</span><span class="o">></span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Subject</span><span class="p">();</span>
<span class="kd">constructor</span><span class="p">(</span><span class="k">public</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">CoffeeListStore</span><span class="p">,</span> <span class="k">private</span> <span class="nx">route</span><span class="p">:</span> <span class="nx">ActivatedRoute</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">ngOnInit</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">this</span><span class="p">.</span><span class="nx">subscribeToQueryParamsUpdates</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">private</span> <span class="nx">subscribeToQueryParamsUpdates</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">route</span><span class="p">.</span><span class="nx">queryParams</span>
<span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">takeUntil</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">ngUnsubscribe$</span><span class="p">))</span>
<span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">params</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">sortCandidates</span><span class="p">(</span><span class="nx">params</span><span class="p">.</span><span class="nx">sort</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">ngOnDestroy</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ngUnsubscribe$</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">ngUnsubscribe$</span><span class="p">.</span><span class="nx">complete</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p><code class="language-plaintext highlighter-rouge">CoffeeListView</code> is responsible for updating the state whenever <code class="language-plaintext highlighter-rouge">sort</code> query param is updated. It does this by subscribing to an observable of route’s query params (<code class="language-plaintext highlighter-rouge">this.route.queryParams.subscribe(...)</code>) and invoking <code class="language-plaintext highlighter-rouge">this.store.sortCandidates</code> when necessary.</p>
<p>Views should take care of doing the sync in reverse direction too. What this means is that whenever state in store is updated (and this update must be reflected in the URL), it is views that are responsible for making the new state persistent by updating the URL. In practice it looks like this:</p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="code"><pre><span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">state$</span>
<span class="p">.</span><span class="nx">pipe</span><span class="p">(</span><span class="nx">takeUntil</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">ngUnsubscribe$</span><span class="p">))</span>
<span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">state</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">navigate</span><span class="p">([</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">],</span> <span class="p">{</span>
<span class="na">queryParams</span><span class="p">:</span> <span class="p">{</span>
<span class="na">sort</span><span class="p">:</span> <span class="nx">state</span><span class="p">.</span><span class="nx">sort</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></figure>
<h3 id="13-one-way-data-flow">1.3 One-way data flow</h3>
<p>As promised a few paragraphs above, this section will explore what is a good way for data to “flow” through the app. What do I have in mind when I say <em>data</em>? Two things mainly:</p>
<ul>
<li><strong>app’s state</strong> we would like to present to the user and</li>
<li><strong>actions’ payload</strong> needed to update app’s state upon user interaction.</li>
</ul>
<p>One-way data flow is a great pattern to ensure state is consistent across all components making up the app. It became quite popular thanks to React. But this doesn’t mean it is only possible to use this pattern in React apps. On the contrary, I would argue that one-way data flow is a great pattern to use when developing front-end applications regardless of framework. Because data flows in one direction it’s easy to “follow” it around and get a clear picture of how the app works.</p>
<p>Here’s a diagram of how the one-way data flow pattern looks like when applied to the architecture I’m describing in this article:</p>
<div class="image image--centered">
<img src="/images/one_way_data_flow.png" alt="Diagram 1: One-way data flow" class="image__img" />
<p class="image__description">Diagram 1: One-way data flow</p>
</div>
<p>The diagram might look a bit intimidating at first but don’t worry, it’s actually quite simple to understand it if you follow the arrows. Here’s a breakdown of our “data journey”:</p>
<ol>
<li>The store first loads data from an API (more on this later). It then transforms loaded data if necessary and updates its state.</li>
<li>Updated state is pushed to all subscribers (smart components or other stores) - this happens automatically since state is a RxJS observable.</li>
<li>Smart components compute local state (if needed) from updated state.</li>
<li>Updated state and smart components’ local state is propagated to presentational components via <code class="language-plaintext highlighter-rouge">@Input()</code> bindings.</li>
<li>Presentational components compute local state (if needed) from inputs and re-render the state.</li>
</ol>
<p>Upon user interaction data flows like this:</p>
<ol>
<li>Presentational component registers user interaction and emits an event with payload to a smart component via <code class="language-plaintext highlighter-rouge">@Output()</code> binding.</li>
<li>Smart component reacts to emitted event by invoking store’s method with arguments computed form event’s payload.</li>
<li>Store updates its state directly or sends a request to an API to make the update persistent. In this case new state is loaded from the API and stored into state after the request is complete.</li>
<li>… and we’ve come full circle.</li>
</ol>
<p>There is another version of the circle presented in the diagram. In this version presentational components are substituted with query params. There are only two differences in how data flows:</p>
<ul>
<li>Upon state update, view propagates updates to query params instead of presentational components.</li>
<li>State updates are triggered by query params updates instead of user’s actions.</li>
</ul>
<p>This concludes the explanation of one-way data flow. It’s not so hard too keep the state in all parts of the app consistent if data flows in one direction. There’s always just <strong>one source of truth</strong> for a particular piece of state (one of the stores) and there is only <strong>one way to update</strong> this state (via a corresponding store).</p>
<h3 id="14-communication-with-external-systems">1.4 Communication with external systems</h3>
<p>The last pattern I’ll talk about in this part of the article is about how to connect an app with external “systems”, such as server API, browser’s local storage, cookies etc. Although I’ll only provide examples of communication with servers, the two main ideas are the same for other types of external systems:</p>
<ul>
<li>Observable <strong>stores</strong> (or other services containing business logic) should be the <strong>only part of an app that knows about external systems</strong>.</li>
<li>Observable stores should not communicate with external systems directly - a <strong>proxy service</strong> should be used to abstract away communication details.</li>
</ul>
<p>The following examples demonstrate how these ideas translate into practice when communicating with a REST API. The proxy service in this case is called <code class="language-plaintext highlighter-rouge">CoffeeListEndpoint</code> and it is injected into the store as <code class="language-plaintext highlighter-rouge">endpoint</code>. The store uses it to reload a list of candidates on initialization.</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/services/coffee-list.store.ts" target="_blank">coffee-list.store.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">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
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListStore</span> <span class="kd">extends</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">CoffeeListStoreState</span><span class="o">></span>
<span class="k">implements</span> <span class="nx">OnDestroy</span> <span class="p">{</span>
<span class="k">private</span> <span class="nx">ngUnsubscribe$</span><span class="p">:</span> <span class="nx">Subject</span><span class="o"><</span><span class="kc">undefined</span><span class="o">></span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Subject</span><span class="p">();</span>
<span class="k">private</span> <span class="nx">reloadCandidates$</span><span class="p">:</span> <span class="nx">Subject</span><span class="o"><</span><span class="kc">undefined</span><span class="o">></span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Subject</span><span class="p">();</span>
<span class="k">private</span> <span class="nx">storeRequestStateUpdater</span><span class="p">:</span> <span class="nx">StoreRequestStateUpdater</span><span class="p">;</span>
<span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">endpoint</span><span class="p">:</span> <span class="nx">CoffeeListEndpoint</span><span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="k">new</span> <span class="nx">CoffeeListStoreState</span><span class="p">());</span>
<span class="p">}</span>
<span class="nx">init</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">initReloadCandidates$</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reloadCandidates</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">storeRequestStateUpdater</span> <span class="o">=</span> <span class="nx">endpointHelpers</span><span class="p">.</span><span class="nx">getStoreRequestStateUpdater</span><span class="p">(</span>
<span class="k">this</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">reloadCandidates</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reloadCandidates$</span><span class="p">.</span><span class="nx">next</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="k">private</span> <span class="nx">initReloadCandidates$</span><span class="p">():</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">reloadCandidates$</span>
<span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">switchMap</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">.</span><span class="nx">listCandidates</span><span class="p">(</span>
<span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidateList</span><span class="p">.</span><span class="nx">sort</span><span class="p">,</span>
<span class="k">this</span><span class="p">.</span><span class="nx">storeRequestStateUpdater</span>
<span class="p">);</span>
<span class="p">}),</span>
<span class="nx">tap</span><span class="p">(</span><span class="nx">candidates</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span>
<span class="na">candidateList</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidateList</span><span class="p">,</span>
<span class="na">candidates</span><span class="p">:</span> <span class="nx">candidates</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">}),</span>
<span class="nx">retry</span><span class="p">(),</span>
<span class="nx">takeUntil</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">ngUnsubscribe$</span><span class="p">)</span>
<span class="p">)</span>
<span class="p">.</span><span class="nx">subscribe</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Notice how <code class="language-plaintext highlighter-rouge">endpoint.listCandidates</code> is not called directly. This pattern is used to make sure we’ll always <strong>update the state with data from endpoint’s last response</strong> if multiple reload candidates requests are initiated at roughly the same time.</p>
<p>When we want to reload the list of candidates we push a new value (<code class="language-plaintext highlighter-rouge">undefined</code>) into <code class="language-plaintext highlighter-rouge">store.reloadCandidates$</code> stream. This triggers execution of pipeable operators. Inside <code class="language-plaintext highlighter-rouge">switchMap</code> we create a new request and <code class="language-plaintext highlighter-rouge">switchMap</code> replaces previous pending request (if present) with this newly created request. The rest of pipeable operators are only executed when the last request is finished. The state is then updated with the most recent data returned from the endpoint. If a request to the endpoint is not successful the <code class="language-plaintext highlighter-rouge">retry</code> operator is used to resubscribe to <code class="language-plaintext highlighter-rouge">store.reloadCandidates$</code> observable (otherwise the stream would complete and no further reloads could be triggered by pushing new values into <code class="language-plaintext highlighter-rouge">store.reloadCandidates$</code>).</p>
<p>Lets now have a look at how <code class="language-plaintext highlighter-rouge">endpoint.listCandidates</code> is implemented.</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/services/coffee-list.endpoint.ts" target="_blank">coffee-list.endpoint.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">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
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListEndpoint</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">http</span><span class="p">:</span> <span class="nx">HttpClient</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">listCandidates</span><span class="p">(</span>
<span class="nx">sort</span><span class="p">:</span> <span class="nx">Sort</span><span class="p">,</span>
<span class="nx">requestStateUpdater</span><span class="p">:</span> <span class="nx">StoreRequestStateUpdater</span>
<span class="p">):</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">Candidate</span><span class="p">[]</span><span class="o">></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="nx">COFFEE_LIST_CONFIG</span><span class="p">.</span><span class="nx">requests</span><span class="p">.</span><span class="nx">listCandidates</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">params</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">sortHelpers</span><span class="p">.</span><span class="nx">convertSortToRequestParams</span><span class="p">(</span><span class="nx">sort</span><span class="p">),</span>
<span class="p">},</span>
<span class="p">};</span>
<span class="nx">requestStateUpdater</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="p">{</span><span class="na">inProgress</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">http</span>
<span class="p">.</span><span class="kd">get</span><span class="o"><</span><span class="nx">ApiResponse</span><span class="o"><</span><span class="nx">Candidate</span><span class="p">[]</span><span class="o">>></span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> <span class="nx">options</span><span class="p">)</span>
<span class="p">.</span><span class="nx">pipe</span><span class="p">(</span>
<span class="nx">map</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">requestStateUpdater</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="p">{</span><span class="na">inProgress</span><span class="p">:</span> <span class="kc">false</span><span class="p">});</span>
<span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
<span class="p">}),</span>
<span class="nx">catchError</span><span class="p">((</span><span class="na">error</span><span class="p">:</span> <span class="nx">HttpErrorResponse</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">requestStateUpdater</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="p">{</span>
<span class="na">inProgress</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="na">error</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nx">throwError</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>In essence, endpoint constructs request parameters and executes HTTP request via Angular’s <code class="language-plaintext highlighter-rouge">HttpClient</code>. One part that needs some more context are the calls to <code class="language-plaintext highlighter-rouge">requestStateUpdater</code>.</p>
<p><code class="language-plaintext highlighter-rouge">requestStateUpdater: StoreRequestStateUpdater</code> is a function used to update request state in the store. Endpoint from the example above uses it to update the state of a request to <code class="language-plaintext highlighter-rouge">inProgress</code> or <code class="language-plaintext highlighter-rouge">error</code>. Stores use <code class="language-plaintext highlighter-rouge">endpointHelpers.getStoreRequestStateUpdater</code> in order to generate the default <code class="language-plaintext highlighter-rouge">StoreRequestStateUpdater</code>.</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/shared/helpers/endpoint.helpers.ts" target="_blank">endpoint.helpers.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="code"><pre><span class="k">export</span> <span class="kd">function</span> <span class="nx">getStoreRequestStateUpdater</span><span class="p">(</span>
<span class="nx">store</span><span class="p">:</span> <span class="kr">any</span><span class="p">,</span>
<span class="p">):</span> <span class="nx">StoreRequestStateUpdater</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="nx">requestName</span><span class="p">,</span> <span class="nx">requestState</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">store</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="p">...</span><span class="nx">store</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span>
<span class="na">requests</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">store</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">requests</span><span class="p">,</span>
<span class="p">[</span><span class="nx">requestName</span><span class="p">]:</span> <span class="nx">requestState</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Sometimes a more flexible way of updating request state is required. In these cases <code class="language-plaintext highlighter-rouge">requestStateUpdater: CustomRequestStateUpdater</code> can be passed to the endpoint.</p>
<p>For example, calling the <code class="language-plaintext highlighter-rouge">getUpdateCandidateRequestStateUpdater</code> in <code class="language-plaintext highlighter-rouge">CoffeeListStore</code> generates such custom request state updater:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/features/coffee-list/services/coffee-list.store.ts" target="_blank">coffee-list.store.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="code"><pre><span class="p">...</span>
<span class="k">private</span> <span class="nx">getUpdateCandidateRequestStateUpdater</span><span class="p">(</span>
<span class="nx">candidate</span><span class="p">:</span> <span class="nx">Candidate</span>
<span class="p">):</span> <span class="nx">CustomRequestStateUpdater</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">requestState</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span>
<span class="na">candidateList</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidateList</span><span class="p">,</span>
<span class="na">candidates</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidateList</span><span class="p">.</span><span class="nx">candidates</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">c</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nx">candidate</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{...</span><span class="nx">c</span><span class="p">,</span> <span class="na">updateRequest</span><span class="p">:</span> <span class="nx">requestState</span><span class="p">};</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">c</span><span class="p">;</span>
<span class="p">}),</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="p">...</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>This custom request state updater is used to update the candidate’s <code class="language-plaintext highlighter-rouge">updateRequest</code> property with request state passed in via <code class="language-plaintext highlighter-rouge">requestState</code> argument.</p>
<p>This concludes the first part where we explored main concepts and ideas used to create a scalable app architecture. We learned about state management with observable stores, presentational and smart container components, one-way data flow and communication with external systems.</p>
<p>In the next part we’ll dive into less theoretical stuff. I’ll present how to lay out an app’s directory structure and how to organize your source files so that you’ll know exactly where to put different parts that make up your app.</p>
<h2 id="2-modules">2. Modules</h2>
<p>The concept of scalable Angular architecture presented in this article is based on dividing an app into different modules. At the root level this division looks like this:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">app/
├── core/
├── features/
├── layout/
├── shared/
├── styles/
├── testing/
├── views/
└── ...</code></pre></figure>
<p>In this part of the article we’ll define the role of each one of these modules and explore their inner workings and structure. Their <strong>structure is very similar in order to keep the burden of complexity out of the way</strong> while programming, allowing you to focus on the things that matter like specific business logic and good user experience.</p>
<h3 id="21-apps-root">2.1 App’s root</h3>
<p>At the <a href="https://github.com/georgebyte/coffee-election-ng-app-example/tree/master/src/app" target="_blank">root of the app</a> there are the following files whose main purpose is to tie everything together into a nicely working app:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">app/
├── ... (app's other modules)
├── app-initialization.module.ts
├── app-routing.module.ts
├── app.component.html
├── app.component.scss
├── app.component.ts
├── app.config.ts
├── app.constants.ts
└── app.module.ts</code></pre></figure>
<p><code class="language-plaintext highlighter-rouge">AppModule</code> is app’s root module. It imports other modules and declares <code class="language-plaintext highlighter-rouge">AppComponent</code>.</p>
<p><code class="language-plaintext highlighter-rouge">AppComponent</code> is app’s main component. It is app’s “shell” component and usually includes a <code class="language-plaintext highlighter-rouge"><router-outlet></code> directive into which Angular router renders other components.</p>
<p><code class="language-plaintext highlighter-rouge">AppRoutingModule</code> is the main routing module. It initializes Angular router with top-level <code class="language-plaintext highlighter-rouge">Routes</code> config. Other routes are defined in features’ routing modules - more about this later.</p>
<p><code class="language-plaintext highlighter-rouge">AppInitializationModule</code> makes use of Angular’s <code class="language-plaintext highlighter-rouge">APP_INITIALIZER</code> injection token to do some work during Angular initialization process. This is useful for loading app’s settings from a server since the code is executed before rendering anything. Note that your app won’t start until all promises are resolved so doing too much work during initialization might impact the performance of your app negatively. In many scenarios it is better to use route resolvers to load necessary data before rendering route’s component as this doesn’t delay app’s initialization.</p>
<p>Lastly, <code class="language-plaintext highlighter-rouge">app.constants.ts</code> is used to store global constants’ enums and <code class="language-plaintext highlighter-rouge">app.config.ts</code> is used to store global app’s configs like REST API base URL or some defaults.</p>
<h3 id="22-core-module">2.2 Core module</h3>
<p>Core module is dedicated to <strong>singleton providers (observable stores/services) provided in root injector</strong>. Only one instance is created for each one of these services in app’s “lifetime” between page reloads. These services contain business logic used by other core services or app’s features.</p>
<p>An example of such service would be the <code class="language-plaintext highlighter-rouge">user</code> core service which is responsible for holding the state of the logged-in user.</p>
<p>Core module’s directories and files are structured like this:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">core/
├── core-service-example/
│ ├── helpers/ (pure helper functions grouped by related functionalities)
│ │ ├── example.helpers.ts
│ │ └── ...
│ ├── services/ (observable stores, endpoints, regular Angular services (providers) etc.)
│ │ ├── core-service-example-resolver.service.ts (data providers used by Angular router - see https://angular.io/api/router/Resolve)
│ │ ├── core-service-example.endpoint.ts
│ │ ├── core-service-example.store.state.ts
│ │ ├── core-service-example.store.ts
│ │ └── ...
│ ├── types/ (TypeScript types, interfaces and classes)
│ │ ├── type-example.ts
│ │ └── ...
│ ├── core-service-example.config.ts
│ └── (core-submodule-example.module.ts) (optional for larger core submodules)
├── another-core-service-example/
│ └── ... (same as above)
└── core.module.ts</code></pre></figure>
<p>Each core service or, in the case of larger core submodules, each submodule, should be specified in <code class="language-plaintext highlighter-rouge">providers</code>/<code class="language-plaintext highlighter-rouge">imports</code> lists in <code class="language-plaintext highlighter-rouge">CoreModule</code>:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/core/core.module.ts" target="_blank">core.module.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span><span class="nx">CoreSubmoduleExampleModule</span><span class="p">],</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">CoreServiceExampleStore</span><span class="p">,</span> <span class="nx">CoreServiceExampleEndpoint</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoreModule</span> <span class="p">{}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p><code class="language-plaintext highlighter-rouge">CoreModule</code> is imported into <code class="language-plaintext highlighter-rouge">AppModule</code> so that all of its providers are added to app’s root injector.</p>
<h3 id="23-feature-modules">2.3 Feature modules</h3>
<p>Feature module is a module composed of related components, providers, types, constants, routing configs etc. All these <strong>components work together to implement an app’s feature</strong>. Their only concern should be this feature and they <strong>should care as little as possible about other parts of the app</strong>. This means that all of the <strong>connections to the “outside world” are made from features’ services</strong> to services in <code class="language-plaintext highlighter-rouge">CoreModule</code> or by features’ views synching query params’ state with state in features’ stores.</p>
<p><strong>A feature should never communicate with other features directly - this communications should be supported via services in <code class="language-plaintext highlighter-rouge">CoreModules</code>.</strong> By adhering to this rule we prevent creating direct dependencies/tight coupling between features. We should be able to remove any non-core feature from our app without breaking other app’s features.</p>
<p>Feature modules live inside <code class="language-plaintext highlighter-rouge">app/features/</code> directory, each module in its own subdirectory, with a structure like this:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">features/
├── feature-example/
│ ├── components/ (presentational components)
│ │ ├── component-example/
│ │ │ ├── component-example.component.html
│ │ │ ├── component-example.component.scss
│ │ │ ├── component-example.component.ts
│ │ │ └── private-type-example.ts (TypeScript type used by this presentational component only)
│ │ └── ...
│ ├── containers/ (container components that CAN'T be routed to, usually used by views to compose more complex interfaces)
│ │ ├── container-example/
│ │ │ ├── container-example.container.html
│ │ │ ├── container-example.container.scss
│ │ │ └── container-example.container.ts
│ │ └── ...
│ ├── helpers/ (pure helper functions grouped by related functionalities)
│ │ ├── example.helpers.ts
│ │ └── ...
│ ├── services/ (observable stores, endpoints, regular Angular services (providers) etc.)
│ │ ├── feature-example.endpoint.ts
│ │ ├── feature-example.store.state.ts
│ │ ├── feature-example.store.ts
│ │ └── ...
│ ├── types/ (TypeScript types, interfaces and classes)
│ │ ├── type-example.ts
│ │ └── ...
│ ├── views/ (container components that CAN be routed to)
│ │ ├── view-example/
│ │ │ ├── view-example.view.html
│ │ │ ├── view-example.view.scss
│ │ │ └── view-example.view.ts
│ │ └── ...
│ ├── feature-example-routing.module.ts
│ ├── feature-example.configs.ts
│ ├── feature-example.constants.ts
│ └── feature-example.module.ts
└── another-feature-example/
└── ... (same as above)</code></pre></figure>
<p>Most of the structure should be understandable if you read the comments in the snippet above and the <a href="#1-main-ideas-and-concepts">first part</a> of this article.</p>
<p>At this point I would like to touch very quickly just on the files at the root of the feature module.</p>
<p><code class="language-plaintext highlighter-rouge">FeatureExampleModule</code> is the definition of the feature module. It imports <code class="language-plaintext highlighter-rouge">FeatureExampleRoutingModule</code> and other app’s modules needed by the feature. It also declares components implemented by the feature. <code class="language-plaintext highlighter-rouge">FeatureExampleModule</code> can be imported directly into <code class="language-plaintext highlighter-rouge">AppModule</code> or it can be lazy loaded by Angular router.</p>
<p><code class="language-plaintext highlighter-rouge">FeatureExampleRoutingModule</code> contains route definitions introduced by the feature, for example:</p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="code"><pre><span class="kd">const</span> <span class="nx">routes</span><span class="p">:</span> <span class="nx">Routes</span> <span class="o">=</span> <span class="p">[{</span>
<span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">feature</span><span class="dl">'</span><span class="p">,</span>
<span class="na">children</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">path</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span> <span class="c1">// Default view for this feature accessible at /feature</span>
<span class="na">component</span><span class="p">:</span> <span class="nx">FirstExampleView</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">second</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// Another view for this feature accessible at /feature/second</span>
<span class="na">component</span><span class="p">:</span> <span class="nx">SecondExampleView</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">...</span>
<span class="p">]</span>
<span class="p">}];</span>
<span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="na">imports</span><span class="p">:</span> <span class="p">[</span><span class="nx">RouterModule</span><span class="p">.</span><span class="nx">forChild</span><span class="p">(</span><span class="nx">routes</span><span class="p">)],</span>
<span class="na">exports</span><span class="p">:</span> <span class="p">[</span><span class="nx">RouterModule</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeListRoutingModule</span> <span class="p">{}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>There should be a route definition specifying <code class="language-plaintext highlighter-rouge">path</code> for every view component inside <code class="language-plaintext highlighter-rouge">views</code> directory. In larger applications it is usually a good idea create a URL “namespace” assigned to feature’s route definitions by using <a href="https://angular.io/api/router/Routes#componentless-routes" target="_blank">componentless routes</a>.</p>
<p>At last, feature’s constants and configs store feature specific constants’ enums and configs, similarly to global app’s constants and configs.</p>
<h3 id="24-shared-module">2.4 Shared module</h3>
<p><code class="language-plaintext highlighter-rouge">SharedModule</code> is a place to store all the <strong>reusable presentational components</strong>, directives, pipes, helpers and types used by features to create their UI. <strong>No component, directive or anything else from <code class="language-plaintext highlighter-rouge">SharedModule</code> should depend on any other module, component, provider etc.</strong> There should be <strong>no business logic</strong> implemented by the members of <code class="language-plaintext highlighter-rouge">SharedModule</code>. You should think about <code class="language-plaintext highlighter-rouge">SharedModule</code> as of a private <code class="language-plaintext highlighter-rouge">node_modules</code> collection of reusable functionalities.</p>
<p>Shared module’s directories and files are structured like this:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">shared/
├── components/
│ ├── complex-component-example/ (larger and more complex reusable components with many sub-components)
│ │ ├── components/
│ │ │ ├── simple-component-example/
│ │ │ │ ├── component-example.component.html
│ │ │ │ ├── component-example.component.scss
│ │ │ │ ├── component-example.component.ts
│ │ │ │ └── private-type-example.ts
│ │ │ └── ...
│ │ ├── types/
│ │ │ ├── type-example.ts
│ │ │ └── ...
│ │ ├── complex-component-example.component.html
│ │ ├── complex-component-example.component.scss
│ │ ├── complex-component-example.component.ts
│ │ └── complex-component-example.module.ts
│ ├── simple-component-example/
│ │ ├── component-example.component.html
│ │ ├── component-example.component.scss
│ │ ├── component-example.component.ts
│ │ └── private-type-example.ts
│ └── ...
├── directives/
│ ├── directive-example.directive.ts
│ └── ...
├── helpers/
│ ├── example.helpers.ts
│ └── ...
├── pipes/
│ ├── pipe-example.pipe.ts
│ └── ...
├── types/
│ ├── type-example.ts
│ └── ...
└── shared.module.ts</code></pre></figure>
<h3 id="25-layout-module">2.5 Layout module</h3>
<p>Layout module is a place where I recommend putting the components like header and footer used to create basic app layout. These components are usually rendered in the UI at all times and are often included directly into root <code class="language-plaintext highlighter-rouge">AppComponent</code>:</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/app.component.html" target="_blank">app.component.html</a>
</span></p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="code"><pre><span class="nt"><ce-header</span> <span class="na">class=</span><span class="s">"ce-header"</span><span class="nt">></ce-header></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"ce-view-container"</span><span class="nt">></span>
<span class="nt"><router-outlet></router-outlet></span>
<span class="nt"></div></span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Directory structure of the layout module is not too complicated:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">layout/
├── footer/
│ ├── footer.component.html
│ ├── footer.component.scss
│ └── footer.component.ts
├── header/
│ ├── header.component.html
│ ├── header.component.scss
│ └── header.component.ts
└── layout.module.ts</code></pre></figure>
<h3 id="26-views-module">2.6 Views module</h3>
<p>Views module is another module that usually turns out to be quite simple. It holds the views that are not a part of any feature and whose routing paths are defined in <code class="language-plaintext highlighter-rouge">AppRoutingModule</code>. An example of such a view is a 404 page (<code class="language-plaintext highlighter-rouge">PageNotFoundView</code>).</p>
<p><span class="highlight-filename">
<a href="https://github.com/georgebyte/coffee-election-ng-app-example/blob/master/src/app/app-routing.module.ts" target="_blank">app-routing.module.ts</a>
</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="code"><pre><span class="kd">const</span> <span class="nx">appRoutes</span><span class="p">:</span> <span class="nx">Routes</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">{</span>
<span class="na">path</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">redirectTo</span><span class="p">:</span> <span class="nx">coffeeListRoutingPaths</span><span class="p">.</span><span class="nx">coffeeList</span><span class="p">,</span>
<span class="na">pathMatch</span><span class="p">:</span> <span class="dl">'</span><span class="s1">full</span><span class="dl">'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">{</span><span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">**</span><span class="dl">'</span><span class="p">,</span> <span class="na">component</span><span class="p">:</span> <span class="nx">PageNotFoundView</span><span class="p">},</span>
<span class="p">];</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Views module’s directory structure is the following:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">views/
├── page-not-found/
│ ├── page-not-found.component.html
│ ├── page-not-found.component.scss
│ └── page-not-found.component.ts
└── views.module.ts</code></pre></figure>
<h3 id="27-styles">2.7 Styles</h3>
<p>Directory <code class="language-plaintext highlighter-rouge">styles/</code> at app’s root is not exactly holding a styles module. It is used to store all the files with app’s global styles definitions. I won’t go into details here as CSS architecture is a topic for an article on its own.</p>
<h2 id="3-testing">3. Testing</h2>
<p>If there’s one thing you SHOULDN’T remember from this article is the lack of unit tests. I allowed myself to omit them in the <a href="https://github.com/georgebyte/coffee-election-ng-app-example" target="_blank">Coffee Election example app</a> for clarity since directory structure snippets would get even more verbose. I would not allow myself to do such a thing in a production app! 🧐</p>
<p>In general I find it best to put the test/spec files right next to files containing the implementation of components, services and other code under test. For example, I would put the test suite to test the <code class="language-plaintext highlighter-rouge">CoffeeListStore</code> in the same directory <code class="language-plaintext highlighter-rouge">features/coffee-list/services</code> where the store is located and use the <code class="language-plaintext highlighter-rouge">.spec.ts</code> suffix to indicate the file contains unit tests:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">features/coffee-list/services/
├── coffee-list.store.ts
└── coffee-list.store.spec.ts</code></pre></figure>
<p>A lot of the times unit testing requires us to mock/stub certain building blocks of the app. I like to put these stubs in <code class="language-plaintext highlighter-rouge">testing/</code> directory at the root of the app (where all other modules like <code class="language-plaintext highlighter-rouge">core</code> and <code class="language-plaintext highlighter-rouge">features</code> are located) and use the <code class="language-plaintext highlighter-rouge">.stub.ts</code> suffix to indicate a file containing mocked/stubbed component or service:</p>
<figure class="highlight"><pre><code class="language-plain" data-lang="plain">testing/
├── component-example.component.stub.ts
└── service-example.service.stub.ts</code></pre></figure>
<p>As the content of this <code class="language-plaintext highlighter-rouge">testing</code> directory expands it becomes useful to organize stub files into more subdirectories.</p>
<p>There’s a lot more to be written about testing but since this article is already quite lengthy I’ll draw the line at this point. If you are interested in how to unit test the observable store services used for state management in the architecture described in this article you can learn more by checking out my previous post about <a href="/state-management-in-angular-with-observable-store-services/" target="_blank">observable stores</a>. I would also suggest you check out the official <a href="https://angular.io/guide/testing" target="_blank">Angular documentation</a> to learn more about testing of Angular apps.</p>
<h2 id="4-conclusion">4. Conclusion</h2>
<p>We covered a lot of ground in this article. First we explored the concepts I find very useful when developing front-end applications like component based architecture and one-way data flow. If you remember just one thing from the first part, let it be this: <strong>you should divide your app into smart and presentational components</strong>.</p>
<p>In the second part I laid out an app architecture which I hope will provide some guidance when you’ll find yourself wondering where to put a specific component, service or some other building block in your apps. This architecture is by no means the ultimate silver bullet, but I find it to be <em>good enough</em> to support development of production ready, robust and scalable front-end applications.</p>This article is a collection of my knowledge about building robust and scalable front-end applications in Angular. First part presents the concepts which I've found are worth adopting when developing front-end applications. In the second part I showcase an Angular app architecture built on top of these concepts.State management in Angular with observable store services2018-01-03T00:00:00+00:002018-01-03T00:00:00+00:00https://georgebyte.com/state-management-in-angular-with-observable-store-services<p class="excerpt">
Observable stores are a state management solution for Angular apps implemented using RxJS to mimic Redux architecture. This article explains how to create, use and test these observable store services.
</p>
<p>Effective state management in front-end development is a challenge, especially in larger and more complex single page applications. Right now, Redux is probably the most popular way of managing state. It is based on a few main ideas:</p>
<ul>
<li>One source of truth (app state).</li>
<li>State is modified in a “pure” way via reducers.</li>
<li>Reducers are invoked by emitting events to them.</li>
<li>Interested entities are notified about state updates.</li>
</ul>
<p>At my day job we have a client facing dashboard application built as a hybrid Angular app (running AngularJS and Angular side by side). AngularJS part of the app stores some state in components’ controllers and other in global services (implementing a pub-sub pattern). Every feature manages its state in a slightly different way because there are no clear conventions set about state management. As a consequence, the more features we add, the harder it becomes to ensure the state stays consistent across all components and services.</p>
<p>The process of upgrading to Angular gave us the opportunity to rethink how we tackle state management in the app. We didn’t want to introduce another layer of complexity by adding a state management library to the codebase. New Angular framework, TypeScript, new build system and hybrid app bootstrap already brought a lot of additional complexity to the mix. Instead, we used the ideas from Redux to create <strong>a state management solution that leverages Angular’s (and RxJS’s) features</strong> to do its job.</p>
<p>One could argue that developing a custom solution for state management introduces additional complexity to the codebase too. It would be naive to dismiss such claims. The difference though is in how much of this complexity is added by developing features using Redux versus observable store pattern. The solution we developed is a really stripped down version of Redux. It does not “prescribe” how to handle async actions, how to combine reducers, how to implement middleware etc. Its only role is to provide a simple API to update state object and to subscribe to its updates. Stores are otherwise just good ol’ Angular service classes.</p>
<p>This article explains how one can use the observable store pattern we developed to manage state in Angular apps. The solution was inspired by the following article from Angular University: <a href="https://blog.angular-university.io/how-to-build-angular2-apps-using-rxjs-observable-data-services-pitfalls-to-avoid/" target="_blank">How to build Angular apps using Observable Data Services</a>.</p>
<p>To showcase the usage of observable stores we’ll build a simple app called <em>Coffee election</em> that lets its users vote for their favorite type of coffee and add their own coffee type to the list of candidates. The source code is available on GitHub: <a href="https://github.com/georgebyte/coffee-election" target="_blank">github.com/georgebyte/coffee-election</a>.</p>
<div class="note">
<p class="note__label">Edit (March 20, 2019):</p>
<p>Many readers have been asking about how to best integrate observable store pattern into an app. I've published an article about scalable Angular app architecture which uses observable stores to manage state. The article and accompanying example app should give you a better understanding of how to use the ideas from this article to build production ready front-end apps. It's available here: <a href="/scalable-angular-app-architecture/" target="_blank">Scalable Angular app architecture</a>.</p>
</div>
<h2 id="abstract-store-class-get-it-from-npm-rxjs-observable-store">Abstract <code class="language-plaintext highlighter-rouge">Store</code> class (get it from npm: <a href="https://www.npmjs.com/package/rxjs-observable-store" target="_blank">rxjs-observable-store</a>)</h2>
<p>At the core of observable store pattern is the abstract <code class="language-plaintext highlighter-rouge">Store</code> class. It leverages RxJS to achieve data flow similar to Redux. It is implemented like this:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/store.ts" target="_blank">store.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="code"><pre><span class="k">import</span> <span class="p">{</span><span class="nx">Observable</span><span class="p">,</span> <span class="nx">BehaviorSubject</span><span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">rxjs</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">T</span><span class="o">></span> <span class="p">{</span>
<span class="nx">state</span><span class="na">$</span><span class="p">:</span> <span class="nx">Observable</span><span class="o"><</span><span class="nx">T</span><span class="o">></span><span class="p">;</span>
<span class="k">private</span> <span class="nx">_state</span><span class="na">$</span><span class="p">:</span> <span class="nx">BehaviorSubject</span><span class="o"><</span><span class="nx">T</span><span class="o">></span><span class="p">;</span>
<span class="k">protected</span> <span class="kd">constructor</span> <span class="p">(</span><span class="na">initialState</span><span class="p">:</span> <span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">_state$</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">BehaviorSubject</span><span class="p">(</span><span class="nx">initialState</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">state$</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">_state$</span><span class="p">.</span><span class="nx">asObservable</span><span class="p">();</span>
<span class="p">}</span>
<span class="kd">get</span> <span class="nx">state</span> <span class="p">():</span> <span class="nx">T</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">_state$</span><span class="p">.</span><span class="nx">getValue</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">setState</span> <span class="p">(</span><span class="na">nextState</span><span class="p">:</span> <span class="nx">T</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">_state$</span><span class="p">.</span><span class="nx">next</span><span class="p">(</span><span class="nx">nextState</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>The store’s state (<code class="language-plaintext highlighter-rouge">_state$</code>) is a RxJS <code class="language-plaintext highlighter-rouge">BehaviorSubject</code>. Changing the state means pushing new state object into the <code class="language-plaintext highlighter-rouge">_state$</code> stream via the <code class="language-plaintext highlighter-rouge">setState</code> method. Interested entities can subscribe to state updates by subscribing to the <code class="language-plaintext highlighter-rouge">state$</code> property. It is also possible to get the current state via the <code class="language-plaintext highlighter-rouge">state</code> property without subscribing to state updates.</p>
<p><code class="language-plaintext highlighter-rouge">Store</code> class provides a <strong>unified interface</strong> for all features’ store services to extend. In the next section we’ll have a look at how to use the abstract <code class="language-plaintext highlighter-rouge">Store</code> class to implement an example feature store service.</p>
<h2 id="features-stores">Features’ stores</h2>
<p>Feature specific stores are Angular <code class="language-plaintext highlighter-rouge">Injectable</code>s extending the abstract <code class="language-plaintext highlighter-rouge">Store</code> class:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.store.ts" target="_blank">coffee-election.store.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeElectionStore</span> <span class="kd">extends</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">CoffeeElectionState</span><span class="o">></span> <span class="p">{</span>
<span class="p">...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>In the code snippet above note the <code class="language-plaintext highlighter-rouge">CoffeeElectionState</code> type used when extending the <code class="language-plaintext highlighter-rouge">Store</code>. Specifying <code class="language-plaintext highlighter-rouge">CoffeeElectionState</code> as the store type adds correct type definitions to the generic store.</p>
<p><code class="language-plaintext highlighter-rouge">CoffeeElectionState</code> is a class representing state object with initial values. In the <em>Coffee election</em> example app it looks like this:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election-state.ts" target="_blank">coffee-election-state.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeElectionState</span> <span class="p">{</span>
<span class="nl">candidates</span><span class="p">:</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="kr">number</span><span class="p">}[]</span> <span class="o">=</span> <span class="p">[];</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>One last thing to do to make this simple example work is to add a <code class="language-plaintext highlighter-rouge">super</code> call to <code class="language-plaintext highlighter-rouge">CoffeeElectionStore</code>’s constructor in order to correctly initialize the state when creating a new instance of <code class="language-plaintext highlighter-rouge">CoffeeElectionStore</code>:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.store.ts" target="_blank">coffee-election.store.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="kd">constructor</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="k">new</span> <span class="nx">CoffeeElectionState</span><span class="p">());</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>With the above code in place, each instance of <code class="language-plaintext highlighter-rouge">CoffeeElectionStore</code> has a way of setting its state and getting the current state or an observable of the state. To make it more useful, additional methods to modify the state (similar to Redux reducers) should be added:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.store.ts" target="_blank">coffee-election.store.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">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
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Injectable</span><span class="p">()</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeElectionStore</span> <span class="kd">extends</span> <span class="nx">Store</span><span class="o"><</span><span class="nx">CoffeeElectionState</span><span class="o">></span> <span class="p">{</span>
<span class="kd">constructor</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="k">new</span> <span class="nx">CoffeeElectionState</span><span class="p">());</span>
<span class="p">}</span>
<span class="nx">addVote</span> <span class="p">(</span><span class="na">candidate</span><span class="p">:</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="kr">number</span><span class="p">}):</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span>
<span class="na">candidates</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">c</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">c</span> <span class="o">===</span> <span class="nx">candidate</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">{...</span><span class="nx">c</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="nx">c</span><span class="p">.</span><span class="nx">votes</span> <span class="o">+</span> <span class="mi">1</span><span class="p">};</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">c</span><span class="p">;</span>
<span class="p">})</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">addCandidate</span> <span class="p">(</span><span class="na">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">,</span>
<span class="na">candidates</span><span class="p">:</span> <span class="p">[...</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">,</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="mi">0</span><span class="p">}]</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>In the example above <code class="language-plaintext highlighter-rouge">CoffeeElectionStore</code>’s functionality was extended by defining <code class="language-plaintext highlighter-rouge">addVote</code> and <code class="language-plaintext highlighter-rouge">addCandidate</code> methods. In essence, these methods modify the state by pushing new state objects into the observable <code class="language-plaintext highlighter-rouge">state$</code> stream via the <code class="language-plaintext highlighter-rouge">setState</code> method.</p>
<p>Note how it is <strong>impossible to modify the state without notifying listeners about the change</strong>. This characteristic of observable stores makes them a perfect fit for implementing one-way data flow in Angular apps - much like with Redux or a similar state management library.</p>
<h2 id="using-injectable-store-services">Using injectable store services</h2>
<p>App’s state could all be stored in a single global state object. But as the app grows, so does the state object and it can quickly become too big to easily extend it with new features. So instead of storing the whole state in one place, it is better to <strong>split the state into smaller chunks</strong>. A good way to split the properties is to group them by feature and extract these groups into separate state objects, managed by corresponding stores.</p>
<p>There are two types of stores that emerge from splitting:</p>
<ul>
<li>global stores that contain globally used state,</li>
<li>component stores that contain the states used by a single component.</li>
</ul>
<p>To set up a <strong>store containing global state</strong> accessed by different services and components, the store is listed as a provider in a module’s providers list (root app module or a feature specific module). This way Angular adds a new global provider to its dependency injector. The state in global stores will be available until the page is reloaded.</p>
<p><span class="highlight-filename">app.module.ts</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">NgModule</span><span class="p">({</span>
<span class="p">...</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">ExampleGlobalStore</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">AppModule</span> <span class="p">{</span>
<span class="p">...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Note that <strong>many global stores can be defined</strong> as providers in app’s modules, each managing its own subset of global state. The codebase stays much more maintainable this way, since each store follows the principle of single responsibility.</p>
<p>To use a global store in different parts of the app, the store needs to be defined as their dependency. This way Angular injects the <strong>same instance</strong> of a global store (defined as singleton provider in <code class="language-plaintext highlighter-rouge">AppModule</code> or any other module) into every component/ service depending on it.</p>
<p><span class="highlight-filename">example.component.ts</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Component</span><span class="p">({</span> <span class="p">...</span> <span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ExampleComponent</span> <span class="p">{</span>
<span class="kd">constructor</span> <span class="p">(</span><span class="k">private</span> <span class="nx">exampleGlobalStore</span><span class="p">:</span> <span class="nx">ExampleGlobalStore</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// ExampleComponent has access to global state via exampleGlobalStore reference</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Not all state needs to be global though. <strong>Component specific state</strong> should only exist in memory if a component is using it. Once user navigates to a different view and the component is destroyed, its state should be cleaned-up too. This can be achieved by adding the store to a list of component’s providers. This way we get “self-cleaning” stores that are kept in memory as long as components using them are kept in memory.</p>
<p><span class="highlight-filename">example.component.ts</span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Component</span><span class="p">({</span>
<span class="p">...</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">ExampleComponentStore</span><span class="p">],</span>
<span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">ExampleComponent</span> <span class="p">{</span>
<span class="p">...</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Private component stores are used in the same way as global stores by defining them as dependencies in the components’ constructors. The key difference is that these <strong>component specific stores are not singletons</strong>. Instead, Angular creates a new instance of the store each time a component depending on it is created. As a consequence, multiple instances of the same component can be present in the DOM at the same time, each one of them having its own store instance with its own state.</p>
<h2 id="subscribing-to-state-updates-in-components-and-services">Subscribing to state updates in components and services</h2>
<p>Once a store instance is injected into a component or service, this component/ service can subscribe to state updates. In the example of <code class="language-plaintext highlighter-rouge">coffee-election</code> component, subscribing to state updates looks like this:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.component.ts" target="_blank">coffee-election.component.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="p">@</span><span class="nd">Component</span><span class="p">({</span> <span class="p">...</span> <span class="p">})</span>
<span class="k">export</span> <span class="kd">class</span> <span class="nx">CoffeeElectionComponent</span> <span class="k">implements</span> <span class="nx">OnInit</span> <span class="p">{</span>
<span class="kd">constructor</span> <span class="p">(</span><span class="k">private</span> <span class="nx">store</span><span class="p">:</span> <span class="nx">CoffeeElectionStore</span><span class="p">)</span> <span class="p">{}</span>
<span class="nx">ngOnInit</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">state$</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">state</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// Logic to execute on state update</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>It is also possible to only subscribe to updates of a subset of state:</p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="k">this</span><span class="p">.</span><span class="nx">store</span><span class="p">.</span><span class="nx">state$</span>
<span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">state</span> <span class="o">=></span> <span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">)</span>
<span class="p">.</span><span class="nx">distinctUntilChanged</span><span class="p">()</span>
<span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">candidates</span> <span class="o">=></span> <span class="p">{</span>
<span class="c1">// Logic to execute on state.candidates update</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>Note that these <strong>subscriptions must be cleaned up</strong> before a component is destroyed in order to prevent memory leaks. We won’t go into details about unsubscribing in this article. Check out <a href="https://stackoverflow.com/a/41177163" target="_blank">this topic on Stack Overflow</a> to learn more.</p>
<h2 id="subscribing-to-state-updates-in-components-templates">Subscribing to state updates in components’ templates</h2>
<p>In case a component doesn’t execute any logic on state update and it only serves as a proxy to pass the state to its template, Angular provides a nice shortcut to subscribe to state updates directly from templates via the <code class="language-plaintext highlighter-rouge">async</code> pipe. <code class="language-plaintext highlighter-rouge">ngFor</code> in the example below will redraw a list of candidates every time the state is updated.</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.component.html" target="_blank">coffee-election.component.html</a></span></p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="code"><pre><span class="nt"><ul></span>
<span class="nt"><li</span> <span class="na">*ngFor=</span><span class="s">"let candidate of (store.state$ | async).candidates"</span><span class="nt">></span>
<span class="nt"><span></span>{{ candidate.name }}<span class="nt"></span></span>
<span class="nt"><span></span>Votes: {{ candidate.votes }}<span class="nt"></span></span>
<span class="nt"><button</span> <span class="na">(click)=</span><span class="s">"store.addVote(candidate)"</span><span class="nt">></span>+<span class="nt"></button></span>
<span class="nt"></li></span>
<span class="nt"></ul></span>
</pre></td></tr></tbody></table></code></pre></figure>
<p>These subscriptions to state updates via <code class="language-plaintext highlighter-rouge">async</code> pipes are <strong>automatically cleaned up</strong> by the framework upon destroying the component.</p>
<h2 id="unit-testing-the-store">Unit testing the store</h2>
<p>Testing state modifying store methods is pretty straightforward. It consists of three steps:</p>
<ol>
<li>Creating an instance of the tested store and setting up mocked initial state.</li>
<li>Calling a store’s method the test is testing.</li>
<li>Asserting the method updated the state correctly.</li>
</ol>
<p>In practice unit tests to test the store from the <em>Coffee election</em> example look like this:</p>
<p><span class="highlight-filename"><a href="https://github.com/georgebyte/coffee-election/blob/master/src/app/coffee-election.store.spec.ts" target="_blank">coffee-election.store.spec.ts</a></span></p>
<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">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
</pre></td><td class="code"><pre><span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">CoffeeElectionStore</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="na">store</span><span class="p">:</span> <span class="nx">CoffeeElectionStore</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">MOCK_CANDIDATES</span> <span class="o">=</span> <span class="p">[{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Test candidate 1</span><span class="dl">'</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="mi">0</span><span class="p">},</span> <span class="p">{</span><span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Test candidate 2</span><span class="dl">'</span><span class="p">,</span> <span class="na">votes</span><span class="p">:</span> <span class="mi">5</span><span class="p">}];</span>
<span class="nx">beforeEach</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">TestBed</span><span class="p">.</span><span class="nx">configureTestingModule</span><span class="p">({</span>
<span class="na">providers</span><span class="p">:</span> <span class="p">[</span><span class="nx">CoffeeElectionStore</span><span class="p">]</span>
<span class="p">});</span>
<span class="nx">store</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CoffeeElectionStore</span><span class="p">();</span>
<span class="nx">store</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
<span class="na">candidates</span><span class="p">:</span> <span class="nx">MOCK_CANDIDATES</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should correctly add a vote to a candidate</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">store</span><span class="p">.</span><span class="nx">addVote</span><span class="p">(</span><span class="nx">MOCK_CANDIDATES</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">store</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">votes</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">store</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nx">votes</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">6</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">should correctly add a candidate</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">store</span><span class="p">.</span><span class="nx">addCandidate</span><span class="p">(</span><span class="dl">'</span><span class="s1">Test candidate 3</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">store</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">candidates</span><span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="dl">'</span><span class="s1">Test candidate 3</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></figure>
<h2 id="conclusion">Conclusion</h2>
<p>The purpose of this article was to present how one can leverage the built in features of Angular framework to implement a simple yet powerful state management solution. The provided <em>Coffee election</em> example app is very simple, but the concepts it demonstrates can be used to successfully manage state in much bigger and more complex apps.</p>
<p>If you are interested in how to use the ideas from this article to build production ready front-end apps, you should check out my article about <a href="/scalable-angular-app-architecture/" target="_blank">Scalable Angular app architecture</a>.</p>
<div class="note">
<p class="note__label">Edit (March 4, 2018):</p>
<p>Some readers pointed out that different state management libraries (e.g. ngrx) provide the same functionality as observable store services and were wondering why one may use observable store pattern instead of these libraries.</p>
<p>Observable store pattern I described here is a much simpler solution in my opinion.</p>
<p>I used ngrx in the past and I think it is a really good library for state management. But it also takes longer to learn it, because of all the features it supports.</p>
<p>As it turned out at my previous company, one may not need a full blown state management library to manage state even in larger applications. This "stripped down" implementation of Redux pattern covered pretty much all of use cases in our app without introducing much additional complexity. I believe less complexity comes from the fact that observable store services heavily depend on Angular features (dependency injection, async pipes etc.) to do a lot of heavy lifting (e.g. cleaning-up unused state when components are destroyed, creating new instances of stores when needed etc.).</p>
</div>Observable stores are a state management solution for Angular apps implemented using RxJS to mimic Redux architecture. This article explains how to create, use and test these observable store services.A tale of a software engineer: The beginnings2017-08-12T00:00:00+00:002017-08-12T00:00:00+00:00https://georgebyte.com/a-tale-of-a-software-engineer-the-beginnings<p class="excerpt">
It all started with Age of Empires II. Then Wordpress made me enroll in a computer science program at university where I realized I had a bunch of things to learn about software engineering. After 3 years of studies I gathered some real-world experience during student internships, got my first job as a web developer, failed to get another and ultimately landed a full stack engineer position at Zemanta. I learned a bunch trough this journey and cracked the code of "How to be a *real* software engineer?".
<br /><br />
Now, for a more poetic version of the above excerpt just keep reading ...
</p>
<h2 id="chapter-1-pre-university-years">Chapter 1: Pre-university years</h2>
<p>My family got our first PC when I was about seven years old. Truth be told, I used it mainly for gaming. You know, AOE II and stuff. I’ve shown absolutely no interest in software development until I was fourteen or so.</p>
<div class="image image--centered">
<img src="/images/george_byte_first_pc.jpeg" alt="George Byte's first PC" class="image__img" />
<p class="image__description">Rocking high fashion developer outfits from an early age.</p>
</div>
<p>Around the age of fourteen, I started to use the web more frequently. I remember being quite amazed by the websites of that era. They sparked an interest in me. I wanted to create my own website!</p>
<p>But my initial motivation quickly faced an obstacle. I literally didn’t know where to start. HTML, CSS, PHP, web hosting, domain names etc. were some terms that kept me up at night. Well, not really. I gave up because there was just too much to learn to get started.</p>
<p>Luckily quitting wasn’t that easy after all. There were all these amazing websites being developed that were constantly reminding me of my wish to create a website. So I gave it another try.</p>
<p>This time around I came across Wordpress. A pre-made website with simple install instructions. It was magical. It was all I needed. And I had my first website up and running in a week.</p>
<p>Many other hobby websites followed the first one. I was slowly learning web technologies like HTML and PHP by poking around Wordpress source code. It wasn’t too hard to do, but it felt like real programming. I liked the feeling. I liked it enough to let it influence my decision about what career path to pursuit once I’m done with high school. I decided to enroll in a computer science program at the Faculty of Computer and Information Science in Ljubljana.</p>
<h2 id="chapter-2-engineering-studies">Chapter 2: Engineering studies</h2>
<p>Soon after starting my studies in 2010 I realized I knew almost nothing about <em>the real programming</em>. Luckily my solid math foundation from high school proved to be very useful when learning software development and I managed to get the hang of it rather quickly.</p>
<p>During my years at university, I discovered I really enjoy developing software. It didn’t matter what type of software - every project/ assignment got me completely involved after the initial resistance of tackling a new, unknown problem. Because of this, it wasn’t too easy for me to choose what field do I want to specialize in. So I kinda YOLO’d it out - web development seemed like a field that would see some serious growth in the years to come, so I chose it as my preferred career path.</p>
<p>This choice steered my decisions about which classes and projects to tackle in my last year of studies. I enrolled in a class which had us develop a project from start to finish. My team chose to build a web app to make learning history in primary school a more interesting thing to do. We did a great job and I got a recommendation from the lecturer for a student internship we had in plan for the last semester.</p>
<h2 id="chapter-3-student-internships">Chapter 3: Student internships</h2>
<p>With the recommendation, I landed a paid internship at DRAGON Ticketing - a company developing ticketing and access control systems for sports events, concerts, art exhibitions etc. They offered me a full-time job after 10 weeks of student internship have passed. This was my first position as a web developer. I’ve customized our ticket selling platform to suit more than 25 different designs from our customers and learned CSS and HTML by heart in the process.</p>
<p>At the same time, I applied for another student internship organized by the faculty. I joined Celtra working on a project called “Using Creativity to Gain Practical Knowledge: Automation Testing of Web Applications”. My main task was to develop a style guide/ design pattern library published on GitHub pages.</p>
<p>I learned a lot about web development during these internships. The experience gathered boosted my confidence as a web developer. So it was time for me to move on and tackle new challenges …</p>
<h2 id="chapter-4-my-first-real-job-application">Chapter 4: My first real job application</h2>
<p>Joining a successful company as a web developer seemed like a solid next step. I applied for a front-end developer position at Outfit7. I spent 10 days (*cough* nights) learning AngularJS like a mad man in order to finish the interview assignment. I did pretty well at my engineering interview and my hopes were high. But it turned out I wasn’t the right “cultural fit”.</p>
<p>To be honest, I wasn’t too sad because this AngularJS hackathon once again ignited my passion for web development.</p>
<h2 id="chapter-5-javascript-madness">Chapter 5: JavaScript madness</h2>
<p>I had a solid knowledge of writing software from my years in college, but I wanted to be able to call myself a <em>real</em> front-end engineer. Learning all the quirks and design choices of JavaScript seemed a step in the right direction.</p>
<p>I created a plan. I read a bunch of books about JavaScript and watched many video tutorials. I subscribed to RSS feeds of a lot of front-end related websites and I was reading their articles on my commute almost all the time. I was eagerly absorbing all the information about the technologies. And I tried to apply my new learnings to projects at work.</p>
<h2 id="chapter-6-linkedin-works">Chapter 6: LinkedIn works</h2>
<p>The more I learned, the more confident I was as a front-end engineer. Once enough of this confidence accumulated, I changed my LinkedIn headline to call myself a front-end engineer. Many interview offers started to appear in my inbox. Most of them from companies abroad. I didn’t want to move so I turned them down. But one day I got an email from Zemanta’s tech lead.</p>
<h2 id="chapter-7-zemanta">Chapter 7: Zemanta</h2>
<p>I got invited to an interview. A four-hour interview. I was scared because I knew Zemanta employed some of the best engineers in Slovenia. I was sure they would manage to crack my shell of a <em>wannabe web developer</em> and expose all my shortcomings.</p>
<p>Sure thing, this didn’t happen. I actually enjoyed the interview challenges and questions. I realized I know a lot about software development. Interviewers at Zemanta must have thought the same since they later offered me a position I gladly accepted.</p>
<p>I kept improving as a front-end engineer for the first year at Zemanta (AngularJS, front-end architecture etc.). After a while, I started to notice that I was reading less front-end related articles from my RSS feeds. At the beginning, everything was interesting to read about and learn. With time, however, less and less new stuff appeared in my RSS feeds. I realized I was slowly getting over JavaScript fatigue. I established a solid base of front-end know-how. And this foundation enables me to quickly pick up a new shiny framework/ technology if needed.</p>
<p>In order to progress further in my engineering career, I had to adjust my learning course. I wanted to expand my knowledge of web development from front-end to back-end and become a more well-rounded full stack engineer.</p>
<p>This meant I had to learn more about server-side technologies, APIs, software architecture, project research and management etc. And that’s what’s keeping me busy for the last few months.</p>
<h2 id="chapter-8-keep-calm-and-keep-learning">Chapter 8: Keep calm and keep learning</h2>
<p>I’m sure you managed to recognize the common denominator of my career path if you made it this far in the post. It’s all about passion for learning new things and experimenting with them. As the time goes by it becomes easier to distinguish between important stuff to learn and some overly-hyped stuff you don’t need to know everything about. Only then can you start to really enjoy the era of fast innovation and not worry about burning out as a software engineer.</p>It all started with Age of Empires II. Then Wordpress made me enroll in a computer science program at university where I realized I had a bunch of things to learn about software engineering. After 3 years of studies I gathered some real-world experience during student internships, got my first job as a web developer, failed to get another and ultimately landed a full stack engineer position at Zemanta. I learned a bunch trough this journey and cracked the code of "How to be a *real* software engineer?". Now, for a more poetic version of the above excerpt just keep reading ...