All pages
Powered by GitBook
1 of 4

Loading...

Loading...

Loading...

Loading...

Render cycle

Atomico optimizes the execution of your script by minimizing resources through the rendering control.

Atomico's render cycle works like this:

First, when the component is instantiated, 3 promises will be created pending resolution:

  1. componentInstance.mounted: resolved once the connectedCallback has been executed by the customElement.

  2. componentInstance.updated: the render cycle for first mount or update is encapsulated in componentInstance.updated.

  3. componentInstance.unmounted: resolved once the disconnectedCallback has been executed by the customElement.

Render or rerender cases are:

  1. First render.

  2. Updating a property or attribute.

  3. Update dispatched by a hook

Remember all updates are stacked into a single big loop of componentInstance.updated.

This improves the testing experience since to observe the changes you only have to wait for the resolution of componentInstance.updated , example:

import { html } from "atomico";
import { expect } from "@esm-bundle/chai";
import { fixture } from "atomico/test-dom";
import { Component } from "./component.js";

describe("my test", () => {
  it("my-component", async () => {
    const componentInstance = fixture(html`<${Component}>
        <span>content...</span>
    </${Component}>`);

    await componentInstance.updated; // fist render

    componentInstance.myProp1 = 10;
    componentInstance.myProp2 = 20;
    componentInstance.myProp3 = 20;

    await componentInstance.updated; // now we can observe the effects on the DOM from the previous updates
    
    await componentInstance.unmounted; // the component has been unmounted
  });
});

Webcomponent as function

The first rendering as the update of a prop will call to execute again the function that defines our component, Atomico internally stores the references to support the Hooks api and will render the virtualDOM returned by said function, this is encapsulated within componentInstance.updated

function component(){
    useEffect(()=>{
        console.log("Component mounted");
        ()=> console.log("Component unmounted");
    }, []);
    return <host/>
}

SSR

Atomico allows modifying its life cycle in cases of SSR, improving the rendering of the CSS in case of SSR and avoiding the effects of useEffect and useLayoutEffect

import {options} from "atomico";

// replace the internal use of CSS StyleSheet by the style tag
options.sheet = false; 

// will avoid hook side effects
options.ssr = true; 

Hydration

Technique to reuse the DOM generated by the SSR, Atomico only in the first render of the component that declares date-hydrate will retrieve the existing DOM in the DOM not created by Atomico to relate it to the virtualDOM, avoiding creating the node again.

Optimization

Atomico by default has a good performance, but it can be further optimized if certain techniques are applied.

Render optimization with static nodes

const staticDom = (
  <host shadowDom>
    <slot />
  </host>
);
function component() {
  return staticDom;
}
function component() {
  return html`<host shadowDom>
    <slot />
  </host>`;
}

A static node is equivalent to using node.cloneNode(true)

render optimization with renderOnce

function component() {
  return <host shadowDom renderOnce>
    <slot />
  </host>
}

The renderOnce property renders the node only once, one of the advantages of this technique is that the virtualDOM can access the scope of the function.

atomico/test-dom

fixture

function that allows to keep the render on a single node for each execution of it, the container will be unmounted at the end of each execution of it.

import { html } from "atomico";
import { expect } from "@esm-bundle/chai";
import { fixture } from "atomico/test-dom";
import { Component } from "./component.js";

describe("my test", () => {
  it("my-component", async () => {
    const component = fixture(html`<${Component}>
        <span>content...</span>
    </${Component}>`);

    await component.updated;

    /** Test logic */
  });
});

This allows each fixture execution inside it to be related to the container used for the test, allowing you to fily test external DOM changes, example:

import { html } from "atomico";
import { expect } from "@esm-bundle/chai";
import { fixture } from "atomico/test-dom";
import { Component } from "./component.js";


describe("my test", () => {
  it("my-component", async () => {
    // first instance of the render, it will return the component
    const component = fixture(html`<${Component}>
        <span>content...</span>
    </${Component}>`);

    await component.updated;
    
    // updates the content of the span tag of the first instance of the render
    fixture(html`<${Component}>
        <span>new content...</span>
    </${Component}>`);
    
    await component.updated;

    expect(
          component.querySelector("span").textContent
    ).to.equal("new content...");
  });
});

Testing

We recommend the @web/test-runner tool, if your project was created with the command npm init @atomico the test environment will be preconfigured, you can see this pre-configuration at https://github.com/atomicojs/base/tree/2-started.

Advanced (Documentation in progress)

To scale the test environment you can complement with tools such as:

  1. Cypress: allows you to automate interactions and evaluate interactions of your interface.

  2. Percy: Allows you to evaluate the appearance changes of your component or application between versions.

Render cycle
atomico/test-dom
atomico/test-hooks

atomico/test-hooks

Build test to custom hooks created with Atomico with an isolated instance and predictable

The "atomico/test-hooks" submodule is a direct part of Atomico's core and will allow you to run the customHook in a controlled way without the need for webcomponents.

import { createHooks } from "atomico/test-hooks";

createHooks

function that creates an isolated context that is shared globally with Atomico hooks when executing the load method

Instance

const hooks = createHooks(opcionalRender, opcionalHost);

Where:

  • optionalRender: Callback that allows restarting the hook's life cycle, this callback will be executed every time a hook like useState or useReducer requests the scope update, Atomico uses it to render the webcomponent again.

  • opcionalHost: allows to share an object for the hook useHost, Atomico uses it to share the instance of the webcomponent.

Return of the instance

interface Hooks {
  load<T>(callback: () => T): T;
  clearEffect(unmounted?: boolean): () => ()=> void;
}

Where:

  • load: Function that allows to load the hooks to the temporary global context of Atomico.

  • clearEffect: Function that activates the collector of useLayoutEffect, when executing clear Effect a callback will be returned that allows ending the collector for useEffect, closing the cycle of secondary effects.

    If clear Effect receives true as parameter, it communicates the unmounted.

Example:

./use-counter.js: customHook to test.

import { useState } from "atomico";

export function useCounter(initialValue) {
  const [value, setValue] = useState<number>(initialValue);
  return {
    value,
    increment: () => setValue((value) => value + 1),
    decrement: () => setValue((value) => value - 1),
  };
}

./use-counter.test.js: The example is based on the test environment from @web/test-runner.

import { expect } from "@esm-bundle/chai";
import { createHooks } from "atomico/test-hooks";
import { useCounter } from "./use-counter.js";

it("Check return", () => {
  const hooks = createHooks();

  const counter = hooks.load(() => useCounter(10));

  expect(counter.value).to.equal(10);
  expect(counter.increment).to.be.an.instanceOf(Function);
  expect(counter.decrement).to.be.an.instanceOf(Function);
});