Melody Documentation
  • Why Melody?
  • Quickstart
    • What is Melody?
    • Tutorial
    • Installation
  • Templates
    • Twig
    • Mounting Components
    • Keys and Loops
    • Attributes
    • CSS Classes
    • Inline Styles
    • Refs
    • CSS Modules
  • Melody-Streams
    • Basic concept
    • A Step-by-Step Walkthrough
  • MELODY-STREAMS API
    • createComponent
    • The component function
  • melody-hooks
    • Overview
    • Rules of Hooks
    • Event Handling
    • Separation of Concerns
  • melody-hooks API
    • useState
    • useAtom
    • useEffect
    • useRef
    • useCallback
    • useMemo
    • useReducer
    • usePrevious
  • melody-component
    • Overview
  • melody-hoc
    • Overview
Powered by GitBook
On this page
  • Description
  • Example
  • The return value
  • Shallow Equal check
  • Returning an Observable
  • Returning an object
  • Delaying a rendering
  • Buffered rendering
  • The input object
  • props: Observable<any>
  • updates: Observable<undefined>
  • subscribe
  • dispatchCustomEvent

Was this helpful?

  1. MELODY-STREAMS API

The component function

Description

The heart of your component logic is the component function. It receives an object which provides access to various streams and a few utilities, and returns an Observable or an object with Observable properties.

Example

import { pluck } from 'rxjs/operators';

function ItemDetails({ props }) {
    const id = props.pipe(pluck(id'));
    // appStore is the global Redux store for the application
    const item = id.pipe(map(id => appStore.getState().items[id]));
    return {
        id,
        item,
    };
}

The return value

Shallow Equal check

Whenever the component function emits a new value Melody will perform a shallow equal check on the state object to determine whether it needs to render the component again. "Shallow Equal" means that all properties are compared against each other (for identity). Unless at least one property has changed, the value would be discarded.

Returning an Observable

If the function returns an Observable (should be compatible with Rx.js) then the values emitted by it will be used for rendering the UI of the component.

Returning an object

The state object can also be provided by returning an object. Melody will wrap it into an observable automatically so that all observable properties of the object are merged into a single stream. This is provided for convenience but also to improve readability.

import { from } from 'rxjs';
import { pluck, map, distinctUntilChanged } from 'rxjs/operators';
import appStore from '@myapp/global-redux-store';

function SelectableItem({ props }) {
    const selected = props.pipe(pluck('selected'), distinctUntilChanged());
    const id = props.pipe(pluck('id'), distinctUntilChanged());
    const item = id.pipe(switchMap(id => from(appStore).pipe(
        map(state => state.items[id]),
        distinctUntilChanged()
    )));
    return {
        id,
        selected,
        item,
        meaningOfLife: 42,
    };
}

In this example, the component will update whenever either id , selected or item emit new values. Since meaningOfLife is not an observable, its value will be available in the template, but changes to it won't be tracked and will not end up in the UI.

Delaying a rendering

Since the return value of the function is an Observable it is possible that it won't immediately emit a value. In these cases, Melody will hold off with rendering the component until a state has been emitted. When returning an object, Melody will wait until all property streams have emitted a value before rendering.

This allows implementing UIs which avoid showing loading animations until needed and similar features.

import { of } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
import { pluck, switchMap, catchError } from 'rxjs/operators';

function Item({ props }) {
    const id = props.pipe(pluck('id'));
    const details = id.pipe(
      switchMap(loadItem)
    );

    return {
        id,
        details
    };
}

// not really relevant but provided here for clarity
// could also use `fetch` directly
function loadItem(id) {
    return fromFetch(`/item/${id}`).pipe(
        switchMap(response => {
          if (response.ok) {
            return response.json;
          } else {
            // Server is returning a status requiring the client to try something else.
            return of({ error: true, message: `Error ${response.status}` });
          }
        }),
        catchError(err => {
          // Network or other error, handle appropriately
   console.error(err);
   return of({ error: true, message: err.message })
        })
    );
}

In this case, the Item component will only render after the details of the item have been retrieved from the server. Until then, nothing will be shown.

It is possible to extend this example slightly to show a loading animation after a certain amount of time has passed to provide an even better user experience.

Buffered rendering

Melody will automatically delay the rendering to the next frame. Thus, two or more consecutive state changes will result in only the last state being rendered.

The input object

The component function receives an object which provides access to some special parts of the component. These properties are described in detail here.

props: Observable<any>

An observable of all properties that are being passed into the component through mount:

parent.twig
{% mount './Gallery' as 'gallery' with { images: images, itemId: itemId } %}
Gallery/index.js
import { createComponent } from "melody-streams";
import template from "./gallery.twig";

const Gallery = ({ props }) => {
    // The component function simply passes the props on
    // to the template, and does nothing else
    return props;
};

export default createComponent(Gallery, template);

Whenever the parent is re-rendered, the props stream will emit an object containing the current properties. In the above example, it would contain a field named images and a field named itemId. These can then be used in the Gallery template:

Gallery/gallery.twig
<div class="gallery">
    <h3>Images for Item {{ itemId }}</h3>
    {% for image in images %}
        <img src="{{ image.url }}" />
    {% endfor %}
</div>

Melody does not do any check to verify that the properties have actually changed. If that is required, the component should take care of it on its own.

A common pattern to select a part of the stream would look like this:

import { pluck, distinctUntilChanged } from 'rxjs/operators';

function Item({ props }) {
    const id = props.pipe(pluck('id'), distinctUntilChanged());
    return { id };
}

If you need the original props in addition to some Observable you create in the component function, you can use combine:

import { combine } from 'melody-streams';
import { pluck, distinctUntilChanged } from 'rxjs/operators';

function Item({ props }) {
    const id = props.pipe(pluck('id'), distinctUntilChanged());
    const details = id.pipe(
      switchMap(loadItem)
    );
    
    return combine(
        props,
        { details }
    );
}

combine takes any number of Observables or objects of Observables, and turns them into an "umbrella Observable" that takes each value from the individual Observables and delivers these values in one single object. This object then becomes the state of the component which is available for rendering.

updates: Observable<undefined>

This observable emits after every rendering of a Melody component. It does not emit an actual value. A common use case is to do some additional initialization on the first mount of the component.

Example

import { take, tap } from 'rxjs/operators';

function Item({ props, updates, subscribe }) {
    // This will log every time the component has been rendered
    // (including the first time)
    subscribe(
        updates.pipe(
            tap(() => console.log('updated!'))
        )
    );
    // This will log only on initial mount
    subscribe(
        updates.pipe(
            tap(() => console.log('mounted!')),
            take(1)
        )
    );
    return props;
}

subscribe

subscribe takes an Observable, calls its subscribe function, and stores the returned unsubscribe handler. When the component is unmounted, Melody will go through all these unsubscribe handlers and call them. Therefore, you have automatic unsubscription, which prevents memory leaks.

const Image = ({ props, subscribe }) => {
    const [rootRef, rootClicks] = attachEvent('click');

    // Because we use subscribe(), the event handler will
    // automatically be unsubscribed when the component unmounts
    subscribe(
        rootClicks.pipe(
            tap(e => {
                logClickEvent(e);
            })    
        ),
    );
    return combine(props, { rootRef });
};

dispatchCustomEvent

PreviouscreateComponentNextOverview

Last updated 5 years ago

Was this helpful?