Development of an isomorphic RealWorld application with SSR and Progressive Enhancement. Part 4 - Components and Composition

  • Tutorial
In the previous part of the tutorial, we solved the problems of isomorphic routing, navigation, fetching, and the initial state of the data. As a result, it turned out to be a rather simple and concise basis for an isomorphic application, which I also allocated to a separate repository - ractive-isomorphic-starterkit . In this part, we will start writing the RealWorld application , but first decompose it. Let's go!
image

Traditional offtopic


Interesting results were shown by a poll about State-based routing. Half of those who voted on the idea did not appreciate what was expected. Still half the idea was still interested, one way or another.

An interesting fact is that just recently I visited Ya. Subbotnik on the frontend. The guys from Yandex seem to have tasted the idea of State-based routing, and it pleases. The speaker who touched on this topic was from the team of affiliate interfaces and, apparently, they look something like this:

...
...

Actually this is no fundamentally different from what I do:

{{#if $route.match(page) && status === 'NEW' && query === 'Yandex'}}
...
{{/if}}

Unless the fact that they use React , which simply has no other expressive means, other than creating components for any piggy, this is its style.

At the end of the report, I specifically once again focused on this part. Then it seemed to me that the speaker was so deeply immersed in the specifics of his project that many could not fully grasp his idea.

Decomposition and composition



If very briefly, then in the context of the frontend, decomposition is the process of breaking a monolithic application into components, each of which solves some part of the problem. The main goals of decomposition are to reduce empirical complexity, as well as to implement the DRY principle .

It is the component approach that dominates the modern front end and is part of the modular approach as a whole. In other words, we divide the code into modules, and the interface into components. Again, in the context of the frontend, a component is a certain user element that encapsulates some part of the UI, business logic, etc. An important component of understanding the components is the way in which decomposition occurs.

There are many approaches to decomposition. For example, Reactuses the principle of “everything is a component”, other frameworks consider components in the context of SOLID , etc. But in general, it all comes down to the desire to make components what is called " high cohesion , loose coupling ".

Despite the fact that they all seem to understand this, but this understanding is very often implemented in different ways. If you take two developers, then, with a high degree of probability, they will decompose the application in different ways. I will only describe the principles of decomposition that I myself adhere to.

Frankly, I don’t think it’s right to “grind” with components and the principles of crushing React components are not very close to me. As a rule, I select components according to the following rules:

  1. Reuse - the use of the same functionality in different parts of the application;
  2. Functional purpose - a clearly defined function and business process with a separate life cycle and condition;
  3. Structural function - improving the structure and increasing the readability of the code

It seems to me that there is no other reason to allocate any part of the application into a separate component. Moreover, excessive decomposition, on the contrary, can complicate the understanding of the project and its support. In other words, balance is very important in this matter.

The whole truth about React (not for the faint of heart)
I venture to grab dislikes with karmic entry, but still I will express my purely subjective opinion (!) Regarding some aspects of React . Or rather, those approaches to which React persuades, and its followers preach.

I will not cry about JSX and how terrible it is - everything has been said about this a thousand times already. And the idea of ​​mixing code and markup does not belong to the react, but rather to PHP , from where, it seems to me, it migrated to the react. Partially.

Here I want to discuss the decomposition principles React inclines to and their direct impact on things like Redux and other Flux. You may be surprised, but I affirm that this was the reason for all those "revolutionary" ideas that the reaction brought. At the same time, breaking in its path all those best practices that have developed over decades of development of the principles of programming.

“Well, what's the salt?” What salt, one .... React "

I think everything, as often happens, started with a great idea - " everything is a component " . This idea is so simple and understandable that it is able to capture the minds of people. Excessive enthusiasm for this simplicity has led to the fact that, in fact, React can not do anything else. He has no other means of expression, except for the creation of components and their composition (here I’m not specifically consideringVirtual DOM and other engine hoods, because they are not so important from the point of view of architecture).

Like any other initially simple and ideal idea, the idea of ​​a reaction also encountered realities. But the creators of React seemed to be so keen on this idea that they began to endure excessive complexity not inside the framework (as others do), but to leave it within the application, preserving the apparent simplicity of their brainchild. And React always answered the same way in any way - just create a component, because everything in your application is a component.

It’s clear that the community has adapted to these principles and even loved them. And yet, it began to make excessive complexity out of its applications, but not in the framework, but in satellite libraries. That is why, as a rule, now we do not write applications on React alone . Behind him, a train of all kinds of add-ons and crutches certainly stretches.

Separately, I note that I am also not a big fan of such "all-in-one" solutions as Angular . I think that the prerogative of frameworks is all that is connected with architectural issues of applications, issues of decomposition and composition, communication between components. But not questions of sending http-requests and the like. For me, Angular is too much, and React- too little.

But back to the decomposition and the principle of "just create another component." As a result, all this wonderful, in essence, idea led to two main things:

  1. Since any problem is solved by creating a component, there are a lot of components, they are small, so mixing markup and code does not look too vulgar;
  2. Due to the strong fragmentation of the application into components, the composition of the components becomes unnecessarily complicated, which makes it difficult to communicate between components of different levels. Especially in combination with the principle of “one-way data flow” . This leads to the fact that communication through a global state becomes the most obvious solution.

Thus, it was manic observance of the principle of “everything is a component” and the absence of other tools that led React first to uncontrolled decomposition, then to complicating the composition of the components, and after that even the grass did not grow. You can cross out the generally accepted principle of code separation and markup, and convince your followers that it's cool when everything is in a heap. We can switch to the use of global states, while for many years we tried to isolate and encapsulate these states. In short, do any kind of madness, just to keep the foundation unshakable.

If you do not agree with my opinion or would like to correct me in something - always welcomein the comment. In general, I think that a lively discussion is a much more productive thing than a silent vyser in karma. Thanks in advance for your courtesy. I myself did not want to offend or offend anyone.

So, for starters, let's decompose the main page and user profile page. I want to realize them first of all.

Home page




Here I highlighted the corresponding components with colored frames:

  • Purple frame - the root component (root) or application component;
  • The blue frame is a component of the tag list;
  • Red frame - component of the list of articles;
  • Beige frame - component of adding to favorites.

There is also an embedded pagination component on the main page for the list of articles that did not fit into the screenshot.

Note that the tag component is obviously reusable. As well as the component of adding to favorites.

User profile




There are also components of the list of articles, tags and favorites. From the new here:

  • Green frame - component of the user profile;
  • The yellow frame is a user subscription component.

The pagination component also did not fit into the screenshot, but given that the list of user articles can be long, it is worth considering it here. It becomes apparent that the article list component is also reusable.

I believe that such a separation into components is balanced, minimally sufficient to achieve the goals of decomposition. And at the same time, the composition of the components remains quite simple and manageable.

Component Types




It seems to me that there are 3 main types of components:

  1. Pure components are simple components, the result of which completely depends on the input parameters (by the type of "pure" functions). Perfectly reused and work well in composition with other components;
  2. Autonomous components are complex components that implement some kind of isolated functionality and implement the principles of SOLID . As a rule, such components are used in composition with “pure” components, implement specific business logic, collect data, etc .;
  3. Wrapping components are not isolated components that are most often used to improve the structure of templates, pass parameters down, and so on.

As I have said more than once, in the real world everything is not so clear, therefore, components often have mixed features and this is normal.

Writing a code


Root component


The root component or application component is exactly the Ractive instance that we configure and create in ./src/app.js . It also implements the general application layout (layout) and contains elements that are always present on the screen (header and footer), as well as the layout of the entire application, including routing.

To improve the structure of the templates and break the general layout into smaller parts, we can use the wrapper components described in the previous section. In Ractive, we can make a component non-isolated by setting a simple property:

{
     isolated: false
}

However, the components themselves are not “cheap”, because they contain all these reactive and calculated properties, observers, life cycles, etc. In fact, the Ractive component is a class that has a built-in state and implements some kind of functionality. If our wrapper does not require all this in any form, but is only a structural element designed to simplify our templates, then it is much “cheaper” to use another built-in decomposition mechanism - partials .

I already took out the hat and basement in the partials in the previous article. In the same way, I will implement other parts of the layout that do not meet the characteristics of the component, because "not everything is a component". ;-)

So, at this stage, the root application template will look like this:

./src/templates/app.html

{{>navbar}}
{{#with 
  @shared.$route as $route, 
  {delay: 500} as fadeIn, 
  {duration: 200} as fadeOut 
}}
{{#if $route.match('/login') }}
Login page
{{elseif $route.match('/register') }}
Register page
{{elseif $route.match('/profile/:username/:section?') }}
Profile page
{{elseif $route.match('/') }}
{{>homepage}}
{{else}}
{{>notfound}}
{{/if}}
{{/with}} {{>footer}}

Hereinafter, I use the guidelines for routing the RealWorld project . In those places where the guides do not contain specific recommendations, I will use approaches that I think are right. I will also use the History API routing instead of hash routing, because we are writing an isomorphic application, and URL fragments, as you know, do not go to the server.

In addition, I highlighted two more partials - for layout of the main page and for page 404. Here they are:

./src/templates/partials/homepage.html

Articles list

./src/templates/partials/notfound.html


Now you need to register them in the root component config:

./src/app.js

    partials: {
        ...
        homepage: require('./templates/parsed/homepage'),
        notfound: require('./templates/parsed/notfound')
    },

Full code ./src/app.js
const Ractive = require('ractive');
Ractive.DEBUG = (process.env.NODE_ENV === 'development');
Ractive.DEBUG_PROMISES = Ractive.DEBUG;
Ractive.defaults.enhance = true;
Ractive.defaults.lazy = true;
Ractive.defaults.sanitize = true;
Ractive.defaults.data.formatDate = require('./helpers/formatDate');
Ractive.defaults.data.errors = null;
Ractive.partials.errors = require('./templates/parsed/errors');
Ractive.use(require('ractive-ready')());
Ractive.use(require('ractive-page')({
    meta: require('../config/meta.json')
}));
const api = require('./services/api');
const options = {
    el: '#app',
    template: require('./templates/parsed/app'),
    partials: {
        navbar: require('./templates/parsed/navbar'),
        footer: require('./templates/parsed/footer'),
        homepage: require('./templates/parsed/homepage'),
        notfound: require('./templates/parsed/notfound')
    },
    transitions: {
        fade: require('ractive-transitions-fade'),
    }
};
module.exports = () => new Ractive(options);


I also played a bit with transition animation settings - now they look much better.

Pagination Component


This component is a bright representative of pure components. The result of his work is completely based on the input parameters - component attributes.



Visually, this component looks completely standard, but it’s important for us to decide which side effect it will produce and how exactly we will control this side effect . In the context of an isomorphic application, and with progressive improvement, the answer to this question is obvious - changing the URL.

We must always remember that we need to be able to navigate between pages, even if JS is disabled. In other words, each page must be represented by its own URL (link). In addition, when reloading the page in the browser, we must remain on the selected list page (full support for SSR ).

Since the guidelines do not have any recommendations on how pagination should be reflected in the URL and whether it should be at all, I use the URL Query parameter offset , which will contain the offset in the list. Why offset , not page ? This will be easier, because this is how API pagination works.

?offset=20

Well, create our first component, Ractive . To do this, the Ractive constructor provides us with the static extend () method , which allows us to expand the constructor with new properties and overwrite existing ones, and as a result get a new constructor. Simply put, this is inheritance.

./src/components/Pagination.js

const Ractive = require('ractive');
module.exports = Ractive.extend({
    template: require('../templates/parsed/pagination'),
    attributes: {
        required: ['total'],
	optional: ['offset', 'limit']
    },
    data: () => ({
        total: 0,
        limit: 10,
        offset: 0,
        isCurrent(page) {
            let limit = parseInt(this.get('limit')),
                offset = parseInt(this.get('offset'));
            return offset === ((page * limit) - limit);
        },
        getOffset(page) {
            return (page - 1) * parseInt(this.get('limit'));
        }
    }),
    computed: {
        pages() {
            let length = Math.ceil(parseInt(this.get('total')) / parseInt(this.get('limit')));
            return Array.apply(null, { length }).map((p, i) => ++i);;
        }
    }
});

This component accepts the attributes total (total number of elements in the list), limit (number of elements on the page) and offset (current offset in the list). Based on these properties, the component generates a list of pages, which is implemented as a calculated property of pages . Moreover, if any of the dependent properties changes over time, the calculated property will be automatically recalculated. Conveniently.

./src/templates/pagination.html

{{#if total > 0 && pages.length > 1}}

{{/if}}

In the template, we simply display this list as links. Pay attention to the use of the special join () method on the router. This method merges the passed parameter and its value with the current Query URL , as a result we get a ready-made query string, taking into account the parameters present there. As always, the router itself takes all the work on link processing and we don’t need to worry about it.

The result is a rather small and simple component, the only side effect of which is a change in the URL parameter. This allows you to use this component in a composition with any list. The component implementing the list simply subscribes to the change of the corresponding URL parameter and uses this value for API requests and data output.

Component Tags


This component is also pure. However, unlike Pagination , it has a different premise for this.



The picture shows that the Tags component is really used in many places in the application. It is also obvious that this component works with a certain list of tags. But the most important thing, and it immediately becomes clear that the list of tags depends on the context in which the component is executed. On the main page - this is a list of popular tags, inside the list of articles - these are tags for a specific article and so on. That is why, this component simply cannot be autonomous and requires the transfer to it of a list of articles from the context in which it is used.

./src/components/Tags.js

const Ractive = require('ractive');
module.exports = Ractive.extend({
    template: require('../templates/parsed/tags'),
    attributes: {
        required: ['tags'],
        optional: ['skin']
    },
    data: () => ({
        tags: [],
        skin: 'outline'
    })
});

./src/templates/tags.html

{{#await tags}}
    

Loading...

{{then tags}} {{catch errors}} {{>errors}} {{else}}

No tags

{{/await}}

This component is even simpler. It accepts a list of tags tags and an additional parameter skin - style tags ( outline and filled ).

Tags can accept a list of tags as an array or as a promise and independently resolve it to a tag list. It produces the same side effect as Pagination , - it changes the query parameter tag (again, there are no recommendations in the guidelines). It has no dependencies and can be used anywhere in the application.

About side effect
It is worth paying special attention that, unlike the pagination component, this component does not merge the variable parameter to the rest of the query string, but completely updates it. The fact is that by clicking on a tag, the user will be able to see the updated list of articles that match this tag. Thus, possible manipulations with the previous list, for example, page navigation, should be reset to zero. In this case, pagination will work together with the refinement of the tag, since the Pagination component maintains its parameter to the existing query string.

Let's try to use this component on the main page. First of all, we need to get a list of popular tags from the API. For this, the service working with the API already has a ready-made call and it remains only to write the code that will implement this request. It is also very important to remember support for SSR and other isomorphic pieces described in a previous article .

Now is an interesting point. I most often implement such requests for data acquisition with the help of, attention, calculated properties of components!

image


Why? I am sure you will understand this by the example of the Articles and Profile components . In short - it's awesome how convenient!

So, we are writing a simple function that simply returns a value:

./src/computed/tags.js

const api = require('../services/api');
module.exports = function() {
    const key = 'tagsList',
        keychain = `${this.snapshot}${this.keychain()}.${key}`;
    let tags = this.get(keychain);
    if ( ! tags) {
        tags = api.tags.fetchAll().then(data => data.tags);
        this.wait(tags, key);
    }
    return tags;
};

As you can see, nothing complicated. The function of the computed property is executed in the context of the component to which it is connected. Everything that happens here has already been explained in the previous part . In addition, we finally used the keychain () method from the ractive-ready plugin . This method simply returns the correct path inside the data object, depending on what level of nesting is the component that connected this calculated property.

Now we connect this property to the Root component, connect the Tags component and pass this property to it as an attribute.

./src/app.js

...
Ractive.defaults.snapshot = '@global.__DATA__';
...
    components: {
        tags: require('./components/Tags'),
    },
    computed: {
        tags: require('./computed/tags')
    },
    ...

Full code ./src/app.js
const Ractive = require('ractive');
Ractive.DEBUG = (process.env.NODE_ENV === 'development');
Ractive.DEBUG_PROMISES = Ractive.DEBUG;
Ractive.defaults.enhance = true;
Ractive.defaults.lazy = true;
Ractive.defaults.sanitize = true;
Ractive.defaults.snapshot = '@global.__DATA__';
Ractive.defaults.data.formatDate = require('./helpers/formatDate');
Ractive.defaults.data.errors = null;
Ractive.partials.errors = require('./templates/parsed/errors');
Ractive.use(require('ractive-ready')());
Ractive.use(require('ractive-page')({
    meta: require('../config/meta.json')
}));
const api = require('./services/api');
const options = {
    el: '#app',
    template: require('./templates/parsed/app'),
    partials: {
        navbar: require('./templates/parsed/navbar'),
        footer: require('./templates/parsed/footer'),
        homepage: require('./templates/parsed/homepage'),
        notfound: require('./templates/parsed/notfound')
    },
    transitions: {
        fade: require('ractive-transitions-fade'),
    },
    components: {
        tags: require('./components/Tags'),
    },
    computed: {
        tags: require('./computed/tags')
    }
};
module.exports = () => new Ractive(options);


./src/templates/partials/homepage.html

...
      
...

Full code ./src/templates/partials/homepage.html
Articles list


That's all, and most importantly, the result is pleasing to the eye:


Tags are loaded on the server, the Tags component is rendered during the SSR and, since the list of tags comes with the initial data state, the components are successfully hydrated on the client without a second request to the API. Shine!

Component Articles


The main difference between stand-alone and clean components is that for a stand-alone component to work, just plug it into the parent component and add the corresponding tag to the markup. If necessary, you can pass some settings through the attributes. The same thing works if you need to remove or disable such a component and all the functionality associated with it - just stop using it in the templates and / or delete it from the parent component.

One such component in our application will be the Articles component . This component implements quite a separate functionality of the list of articles and internally uses other components, such as Tags and Pagination .



Component Articles It is used on at least 2 pages of the application (main and profile) and can display 5 types of articles list, depending on the parameters that are transferred to it:

  1. General list of articles
  2. A personal list of articles for the current user based on his subscriptions
  3. List of articles filtered to some tag
  4. List of articles authored by an arbitrary user
  5. List of articles that an arbitrary user added to favorites

Wow! In addition, all of these list types must support pagination, be isomorphic, and work without JS.

In fact, it is in such situations that stand-alone components are a great solution! It allows you to encapsulate all the necessary markup and logic inside the component, exposing only the necessary interface. This eliminates dangerous side effects when using the component in different parts of the application.

Let's start with data fetching :

./src/computed/articles.js

const api = require('../services/api');
module.exports = function() {
    const type = this.get('type'),
        params = this.get('params');
    const key = 'articlesList',
        keychain = `${this.snapshot}${this.keychain()}.${key}`;
    let articles = this.get(keychain);
    if (articles) {
        this.set(keychain, null);
    } else {
        articles = api.articles.fetchAll(type, params);
        this.wait(articles, key);
    } 
    return articles;
};

As you can see, I again wrote a function for a computed property and it is very similar to the same for tags. Read about the benefits of calculated properties voluntarily under the spoiler.

Advantages of the Approach
Сразу оговорюсь, использовать этот подход нужно с умом и только для тех видов данных, которые мы выводим на страницу непосредственно. Так в чем же преимущества использования вычисляемых свойств для фетчинга?

Во-первых, это очень декларативно, сравните:
// императивно фетчим данные в life cycle хуке или еще где-то
oninit () {
    const foo = fetch('/foo').then(res => res.json());
    this.set('foo', foo);
}
// декларативно описываем функцию, которая просто возвращает значение
computed: {
    bar() {
        return fetch('/bar').then(res => res.json());
    }
}

Во-вторых, это «лениво» и при этом не требует никаких дополнительных действий:


{{#if baz}}
    {{foo}}
{{/if}}

{{#if baz}}
    {{bar}}
{{/if}}

В-третьих, автоматически работает с зависимостями:

// императивный подход, где-то там же в хуке
oninit () {
    this.observe('qux', (val) => {
        const foo = fetch(`/foo?qux=${val}`).then(res => res.json());
        this.set('foo', foo);
    });
}
// декларативно все в той же функции
computed: {
    bar() {
        const qux = this.get('qux');
        return fetch(`/bar?qux=${qux}`).then(res => res.json());
    }
}

В-четвертых, функцию вычисляемого свойства можно удобно вынести в отдельный модуль (как это делаю я) и использовать в любом компоненте. Функция выполняется в контексте того компонента к которому она подключена. А также вычисляемому свойству все равно как оно будет именоваться в компоненте:

computed: {
    foo: require('./computed/baz'),
    bar: require('./computed/baz'),
}

If you are still not sure that this is a good idea, please look again at the code for the calculated property of the article list, which I quoted before the spoiler. So, before you literally 10 lines of code and this is the whole business logic of the Articles component ... Isn't that impressive?

Next, we describe the component itself:

./src/components/Articles.js

const Ractive = require('ractive');
module.exports = Ractive.extend({
    template: require('../templates/parsed/articles'),
    components: {
        pagination: require('./Pagination'),
        tags: require('./Tags'),
    },
    computed: {
        articles: require('../computed/articles')
    },
    attributes: {
        optional: ['type', 'params']
    },
    data: () => ({
        type: '',
        params: null
    })
});

Here we connected the nested components, the calculated property and decided on the component interface - it accepts only two attributes that are optional: type (list type, can be either an empty string or 'feed') and params (an object with filtering parameters). The template turned out a little more complicated, because in fact the component is not small:

./src/templates/articles.html

{{#await articles}}

Loading articles...

{{then data}} {{#each data.articles as article}}

{{ article.title }}

{{ article.description }}

Read more...
{{else}}

No articles are here... yet.

{{/each}} {{catch errors}}
{{>errors}}
{{else}}

No articles are here... yet.

{{/await}}

Well, let's spit it on the main page.

./src/app.js

    components: {
        ...
        articles: require('./components/Articles'),
    },

Full code ./src/app.js
const Ractive = require('ractive');
Ractive.DEBUG = (process.env.NODE_ENV === 'development');
Ractive.DEBUG_PROMISES = Ractive.DEBUG;
Ractive.defaults.enhance = true;
Ractive.defaults.lazy = true;
Ractive.defaults.sanitize = true;
Ractive.defaults.snapshot = '@global.__DATA__';
Ractive.defaults.data.formatDate = require('./helpers/formatDate');
Ractive.defaults.data.errors = null;
Ractive.partials.errors = require('./templates/parsed/errors');
Ractive.use(require('ractive-ready')());
Ractive.use(require('ractive-page')({
    meta: require('../config/meta.json')
}));
const api = require('./services/api');
const options = {
    el: '#app',
    template: require('./templates/parsed/app'),
    partials: {
        navbar: require('./templates/parsed/navbar'),
        footer: require('./templates/parsed/footer'),
        homepage: require('./templates/parsed/homepage'),
        notfound: require('./templates/parsed/notfound')
    },
    transitions: {
        fade: require('ractive-transitions-fade'),
    },
    components: {
        tags: require('./components/Tags'),
        articles: require('./components/Articles'),
    },
    computed: {
        tags: require('./computed/tags')
    }
};
module.exports = () => new Ractive(options);


./src/templates/partials/homepage.html

...
          
          {{#if $route.query.tag }}
          
          {{/if}}
...
      
...

Full code ./src/templates/partials/homepage.html


Notice how the implementation of adding a tab with the name of the tag that is being filtered is implemented. If the user clicks on a tag from the Tags component (it doesn’t matter from the list of articles or popular tags), the list of articles is not only filtered by this tag, but a tab with the tag name is also added to visually highlight it.

In general, it works like this and, in my opinion, it did not turn out badly:


And of course, everything is isomorphic, the initial loading occurs without a single ajax request on the client. The browser history is fully functional, and with JS turned off, everything works fine. In short, we get further.

Profile Component


I would like to say that this component will be something outstanding, but no. This plus or minus is the same stand-alone component as Articles, and it will work plus or minus as well. In fact, it is even more boring, as it is used on only one page.



In fact, he is this page. I don’t know how to put it another way.

./src/components/Profile.js

const Ractive = require('ractive');
module.exports = Ractive.extend({
    template: require('../templates/parsed/profile'),
    components: {
        articles: require('./Articles')
    },
    computed: {
        profile: require('../computed/profile')
    },
    attributes: {
        required: ['username'],
	optional: ['section']
    },
    data: () => ({
        username: '',
        section: ''
    })
});

However, the traditional computed property is still a bit more complicated:

./src/computed/profile.js

const api = require('../services/api');
let _profile;
module.exports = function() {
    const username = this.get('username');
    const key = 'profileData', 
        keychain = `${this.root.snapshot}${this.keychain()}.${key}`;
    let profile = this.get(keychain);
    if (profile) {
        this.set(keychain, null);
        _profile = profile;
    } else if (_profile && _profile.username === username) {
        profile = _profile;
    } else if (username) {
        profile = api.profiles.fetch(username).then(data => (_profile = data.profile, _profile));
        this.wait(profile, key);
    }
    return profile;
};

Here I’m kind of “caching” the user profile in the closure ( _profile ), since I don’t want to request the user profile again when switching to the Favorited Articles subroutine and vice versa. It is not difficult and not expensive, but it works well. For example, in the implementation of React / Redux, this issue has not been resolved, and therefore, each time you switch between “My Articles” and “Favorited Articles”, a profile is fetched. Immediately evident the guys did not try.

Now we use all this economy in the template:

./src/templates/profile.html

{{#await profile}} {{then profile}} {{catch errors}} {{>errors}} {{/await}} {{#if username}} {{/if}}

Then everything is as usual - add to the Root component and in the corresponding route.

./src/app.js

    components: {
        ...
        profile: require('./components/Profile'),
    },

Full code ./src/app.js
const Ractive = require('ractive');
Ractive.DEBUG = (process.env.NODE_ENV === 'development');
Ractive.DEBUG_PROMISES = Ractive.DEBUG;
Ractive.defaults.enhance = true;
Ractive.defaults.lazy = true;
Ractive.defaults.sanitize = true;
Ractive.defaults.snapshot = '@global.__DATA__';
Ractive.defaults.data.formatDate = require('./helpers/formatDate');
Ractive.defaults.data.errors = null;
Ractive.partials.errors = require('./templates/parsed/errors');
Ractive.use(require('ractive-ready')());
Ractive.use(require('ractive-page')({
    meta: require('../config/meta.json')
}));
const api = require('./services/api');
const options = {
    el: '#app',
    template: require('./templates/parsed/app'),
    partials: {
        navbar: require('./templates/parsed/navbar'),
        footer: require('./templates/parsed/footer'),
        homepage: require('./templates/parsed/homepage'),
        notfound: require('./templates/parsed/notfound')
    },
    transitions: {
        fade: require('ractive-transitions-fade'),
    },
    components: {
        tags: require('./components/Tags'),
        articles: require('./components/Articles'),
        profile: require('./components/Profile'),
    },
    computed: {
        tags: require('./computed/tags')
    }
};
module.exports = () => new Ractive(options);


./src/templates/app.html

...
{{elseif $route.match('/profile/:username/:section?') }}
  
...

Full code ./src/templates/app.html
{{>navbar}}
{{#with 
  @shared.$route as $route, 
  {delay: 500} as fadeIn, 
  {duration: 200} as fadeOut 
}}
{{#if $route.match('/login') }}
Login page
{{elseif $route.match('/register') }}
Register page
{{elseif $route.match('/profile/:username/:section?') }}
{{elseif $route.match('/') }}
{{>homepage}}
{{else}}
{{>notfound}}
{{/if}}
{{/with}} {{>footer}}


Phew, that's probably enough for today. The current results of the project are here:

Repository
Demo

In the next part, we will work on authorization and isomorphic forms with progressive enhancement . It will be interesting, do not switch!

Also popular now: