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
  • Why Hooks?
  • A first glimpse
  • The State Hook
  • Understanding the execution flow
  • Composing hooks

Was this helpful?

  1. melody-hooks

Overview

Why Hooks?

Hooks enable us to implement components in a reactive way, keeping connected logic co-located. They were initially introduced by React and soon adopted by Melody.

The melody-hooks package is still experimental and might still change in significant ways.

A first glimpse

import { useState, createComponent } from 'melody-hooks';
import template from './index.twig';

// define the state hook
function useCounterState() {
    // useState takes an initial state
    // and returns the value and a mutator function
    const [count, setCount] = useState(0);
    // return the final state that will be available in the template
    return {
        count,
        increaseCounter: () => setCount(count + 1),
        decreaseCounter: () => setCount(count - 1)
    };
}

// combine the state hook with the template and get a component back
export default createComponent(useCounterState, template);

useState is one of the hooks that are provided by the melody-hooks package. It accepts an initial state and returns a tuple of the current value and a mutator function.

Hooks enable us to write stateful component logic in a very concise and elegant way, however, they are also rather dangerous and therefore care needs to be taken. One such principle is that we prefix all hooks, whether provided by melody-hooks or implemented manually, with the word use. That allows us to rely on linting tools, such as eslint-plugin-react-hooks, but also improves the general readability of the code for humans.

In Melody, the function that you pass to createComponent is a hook itself and is therefore also prefixed with the word use. We call this function the components state hook. This particular feature of Melody can't be stressed enough because it is what enables even more composition and code reuse to happen.

In case you're wondering:

A tuple is a collection of values. The term is frequently used in other programming languages which actually support tuples. We're using the term here as well since its used in the React documentation as well but for all intends and purposes when taking about JavaScript a tuple is just an array with well defined parts.

A mutator function is a function which changes state. The term comes from traditional object oriented programming where most properties of a class were hidden behind accessor and mutator methods. You can usually spot a mutator function by their set prefix while accessor functions are usually prefixed with get or is.

The State Hook

The primary Hook of a component is called the State Hook in Melody. Effectively the State Hook is a function which maps the props to the template context while being able to access additional Hooks to persistently store state, register effects and more.

Understanding the execution flow

How does all of this work? Why does it look so magical? In general using Hooks and building custom hooks is a lot easier than understand how they work. It's still worth understanding how they fit into a reactive programming world like Melody.

When the component is mounted, Melody will execute the state hook function. This will trigger useState(0) to be executed. Since it is the first time and no other value has ever been specified for this hook, useState will return a tuple of the current value, 0, and a function that can be used to change the value - the mutator. Thus, the variable count in the example above will contain the value 0.

We also define a few more functions to increase or decrease the counter by 1 and return all of them together as an object. This object will be the context in which our template is executed.

Now, if the user clicks on the + button, setCount(count + 1) will be executed. That changes count to 1. Melody will notice this change and will cause the state hook to be evaluated again.

This time, however, the invocation of useState(0) will return 1, since that's the current state of the hook. Melody kept track of the value stored through the hook during distinct executions of the state hook function.

Composing hooks

One of the things that makes hooks a perfect match for Melody is that we don't need anything except a hook and a template to create a working Melody component. Even the function which maps properties to a state is just a hook.

This enables powerful composition and increases reusability. Let's explore how that works.

Assume, for a moment, that you'd like to make a component selectable. We can achieve that objective by using the useState hook with a boolean value. In order to keep the logic of toggling the selected state local, we can expose a toggle function instead of the plain mutator function.

function useSelectable(props) {
    const [isSelected, setSelected] = useState(props.isSelected);
    return {
        isSelected,
        toggle: () => setSelected(!isSelected)
    };
}
export default createComponent(useSelectable, template);

Now imagine that in addition to being selectable we also want to have a counter. One way to achieve that would be to put all of the logic into a single state hook, however, that wouldn't really help with readability. Instead, we can combine the useCounterState hook with the useSelectable hook to create a new one.

import { useState, createComponent } from 'melody-hooks';
import template from './index.twig';

// define the state hook
function useCounterState(props) {
    // useState takes an initial state
    // and returns the value and a mutator function
    const [count, setCount] = useState(props.count);
    // return the final state that will be available in the template
    return {
        count,
        increaseCounter: () => setCount(count + 1),
        decreaseCounter: () => setCount(count - 1)
    };
}

function useSelectable(props) {
    const [isSelected, setSelected] = useState(props.isSelected);
    return {
        isSelected,
        toggle: () => setSelected(!isSelected)
    };
}

function useSelectableCounter(props) {
    const counter = useCounterState(props);
    const selectable = useSelectable(props);
    return {
        ...counter,
        ...selectable
    };
}

// combine the state hook with the template and get a component back
export default createComponent(useSelectableCounter, template);

Each state hook on its own is a useful component. But you can also combine them to create more complex components.

Let's take things one step further and say that we want to create a component that is selectable, feature a counter mechanism but freezes the counter if it is not selected.

function useCountableOnSelectedState() {
    const { count, increaseCounter, decreaseCounter } = useCounterState({
        count: 0
    });
    const { isSelected, toggle } = useSelectable({ isSelected: false });
    return {
        count,
        isSelected,
        toggle,
        increaseCounter() {
            if (isSelected) {
                increaseCounter();
            }
        },
        decreaseCounter() {
            if (isSelected) {
                decreaseCounter();
            }
        }
    };
}

In this example we've composed a new behaviour out of two already existing behaviours while also modifying their behaviour.

While this is already extremely powerful, the above example could have been implemented in a cleaner way. We already had the composition of countable and selectable and all we really wanted to do was to refine how they work.

With that in mind, let's improve our implementation.

function useCountableOnSelectedState() {
    const state = useSelectableCounter({
        count: 0,
        isSelected: false
    });
    return {
        ...state,
        increaseCounter() {
            if (state.isSelected) {
                state.increaseCounter();
            }
        },
        decreaseCounter() {
            if (state.isSelected) {
                state.decreaseCounter();
            }
        }
    };
}

This type of hook is usually referred to as a decorator hook since it decorates another hook, changing its behaviour but not its public API.

PreviousThe component functionNextRules of Hooks

Last updated 5 years ago

Was this helpful?