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
  • Purpose
  • Syntax
  • Lazy initial value
  • Computing an update

Was this helpful?

  1. melody-hooks API

useState

The useState hook looks incredibly cool in short examples, presentations or during an argument, however, in the vast majority of cases this is not the tool you want to use when managing state! Especially when dealing with event handlers there are too many pitfalls around useState.

Instead, the tool you want to use is the useAtom hook which is based upon useState but avoids the most significant issues.

If you want to use useState directly, make sure to at least use the updater function approach.

Purpose

The useState hook allows you to persistently store data between State Hook executions. It also provides a way to mutate the state.

Syntax

Effectively, the useState function accepts a value - the initial state - which will only be taken into consideration during its very first execution. It returns an array in which the first element represents the current value and the second element represents a mutator function, a function that can be used to change the value.

function useToggleButtonState() {
    const [isSelected, setSelected] = useState(false);
    return { isSelected, setSelected };
}

In this example, we've used the useState hook, providing it with an initial value of false and we've named the current value isSelected and the mutator function is called setSelected.

Thus, during the first execution of this Hook, the value of isSelected will be false (its initial value). But if, at some point, we invoke setSelected(true) then any subsequent execution of this Hook will result in isSelected having the value true.

Lazy initial value

Sometimes it can be expensive to calculate an initial value. In these cases executing the calculation during every execution of the State Hook, just to discard the value after the very first run, would be wasteful. Thus, you can also provide a function as the initial value. It'll be evaluated during the first execution but ignored afterwards.

function useFibonacciLabel() {
    const [fibo, setFibo] = useState(() => fibonacci(15));
    return { fibo, setFibo };
}

By passing a function which calculates the initial state, we avoid calculating the 15th fibonacci number for every execution of the function, thus significantly improving our performance.

Computing an update

Often you'll want to update a value in relation to the current value, especially when it comes to dealing with events. Thus, useState allows you to specify an updater function which receives the current value and returns the new value.

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,
        // the mutator function accepts a function which calculates
        // the new state based on the previous state
        increaseCounter: () => setCount(count => count + 1),
        decreaseCounter: () => setCount(count => count - 1)
    };
}

This pattern is always better than the usual setCount(count + 1) you'll see in marketing material as it eliminates a lot of potential issues consumers of your hook would other wise have. For example, if they start to optimise their code by wrapping the increaseCounter within a useCallback hook, they'd accidentally close over the old value of count, making it impossible to go beyond changing the original value by 1.

By following this approach you also get the opportunity to optimise your code to achieve better performance. Don't worry if you're not familiar with the other hooks used in this example, we'll look at them soon.

// since these are just plain functions we can extract them
// from our hook and give them a nice name
const plusOne = count => count + 1;
const minusOne = count => count - 1;

// 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: useCallback(() => setCount(plusOne), []),
        decreaseCounter: useCallback(() => setCount(minusOne), [])
    };
}

The unoptimised version of the hook was producing a lot of new functions during every single execution and because increaseCounter and decreaseCounter were bound to the current value of count, every usage of those functions had to be rebound during every execution as well. However, now that we are using updater functions and are no longer dependent on the current value of count, we can provide a meaningful name to the updater functions and move them out of the hook (saving 2 function creations per call) as well as use the useCallback hook which will always return the very first version of the function defined for it, which effectively means that additional functions are easy to discard for the JavaScript VM since they're never really used.

Those adjustments result in a much better performance and a much more stable API for your hook.

PreviousSeparation of ConcernsNextuseAtom

Last updated 5 years ago

Was this helpful?