README.md

TNG-Hooks

TNG-Hooks (/ˈting ho͝oks/) is inspired by React Hooks. It's a simple implementation of hooks like useState(..) and useReducer(..) that works for non-React standalone functions. It even supports React's "Custom Hooks" pattern.

Environment Support

This utility uses ES6 (aka ES2015) features. If you need to support environments prior to ES6, transpile it first (with Babel, etc).

At A Glance

TNG-Hooks provides a TNG(..) utility that wraps regular, stand-alone (non-React) functions, providing them the ability to call certain hooks inside them. For instance, useState(..) stores persistent (across invocations) state for each function -- essentially the same as React's useState(..) hook for function components.

In the above snippet, activated is persistent (across invocations) state for the renderUsername(..) function, and expanded is separate persistent state for the onClickUsername(..) function.

Note: Since TNG-Hooks does not currently implement React's useEffect(..) hook, this example is emulating the one-time click handler attachment "side effect" via a persistent activated state, which only runs once.

If a hook like useState(..) is used inside a non-TNG-wrapped function, that function is emulating a React "Custom Hook", and so it must be called from another TNG-wrapped function; otherwise, an error will be thrown. See TNG Custom Hooks below for more information.

There are some important rules to keep in mind with using TNG-Hooks calls in your functions.

Overview

TNG-Hooks is inspired by React's Hooks mechanism. It implements some similar capabilities but for stand-alone (non-React) functions.

TNG(..) is a utility to wrap one or more functions so they are able to maintain a persistent hook context across multiple invocations.

useState(..) Hook

The useState(..) hook utility, like React's useState(..) hook, allows a function to persist some state across multiple invocations, without relying on global variables or having to manually create a closure to store that state. This only works for functions that have been adapted via the TNG(..) wrapper utility to have a hooks context.

The useState(..) function takes a single value, or a function which returns a value. This value is used only the first time, as the initial value for that unit of state.

The return value of useState(..) is a tuple (2-element array) containing the current value of that unit of state, as well as a function to use to set/update that unit of state. You can name this unit of state whatever is appropriate, and also name the set/update function whatever is appropriate.

In the above snippet, we used array destructuring to set count and updateCount from the tuple returned from useState(..).

The setter/updater (updateCount(..) in the above snippet) normally receives a single value. Alternatively, you can pass a function, which will receive the current value of that state unit as its only argument.

This approach is helpful for determining the new state unit value using its current value, especially if, as shown above, the setter/updater function is not inside the closure and cannot access the current state unit value directly.

In this example, the line updateCount(onUpdateCount) could also have been written as:

updateCount( onUpdateCount(count) );

The onUpdateCount(..) is passed the current count value and returns an updated value; that new value is passed directly to updateCount(..) rather than the function.

useReducer(..) Hook

Like React's useReducer(..) hook, the useReducer(..) hook is like a special case of TNG's useState(..) hook in that it also provides for persistent state storage across invocations; it's helpful for certain common cases when the state updates are more involved.

useReducer(..) expects a reducer function and an initial value for its state unit.

Note: Unlike React, TNG does not require or even ask you to name your "custom hooks" in the format useWHATEVER(..) with a use prefix. You can do so if you prefer.

The useHitCounter(..) custom hook -- which again is just a normal non-wrapped function that happens to use useState(..)! -- adopts the hook context of the TNG-wrapped function which invoked it. In this example, the source TNG-wrapped function is either one of the two click handlers (produced via the two TNG(..) calls) that were bound, respectively, to each button.

In other words, the line var [count,updateCount] = useState(0); acts as if it had been called inside of one of the click handlers, even though it's actually in a separate function. That makes useHitCounter(..) a custom hook, that can be called from any number of TNG-wrapped functions.

Hook Call Rules

It is absolutely required that hooks always be called in the same order. That is, that you must never have an invocation of a function that skips over an earlier hook call but then tries to invoke one of the subsequent hook calls. THIS WILL BREAK!

However, it is still technically possible to have hook calls in conditional situations (or even loops!), as long as you are very careful to never skip calls in an unsafe ordering manner.

If you have three hook calls (A, B, and C) in a function, these are the valid call ordering scenarios:

A, B, C

A, B

A

These are invalid scenarios and will break:

B, C

A, C

B

C

To avoid the intricasies of those ordering scenarios, it is strongly recommended that you only call TNG hooks (useState(..), useReducer(..), etc) from the top-level of the function, not inside of any loops or conditionals.

This is considered a best practice in terms of readability of your functions. But it also happens to be the easiest way to ensure that the hooks are always called in the same order, which is critical.

Custom hooks do not have to be named like useXYZ(..) with a use prefix. However, it's a good idea to keep your hooks named that way, to keep in line with conventions from React hooks.

npm Package

npm install tng-hooks

And to require it in a node script:

var { TNG, useState, useReducer, /* .. */ } =require("tng-hooks");

Builds

The distribution library file (dist/tng-hooks.js) comes pre-built with the npm package distribution, so you shouldn't need to rebuild it under normal circumstances.

However, if you download this repository via Git:

The included build utility (scripts/build-core.js) builds (and minifies) dist/tng-hooks.js from source. Note: Minification is currently disabled. The build utility expects Node.js version 6+.

To install the build and test dependencies, run npm install from the project root directory.

Note: This npm install has the effect of running the build for you, so no further action should be needed on your part.

To manually run the build utility with npm:

npm run build

To run the build utility directly without npm:

node scripts/build-core.js

Tests

A comprehensive test suite is included in this repository, as well as the npm package distribution. The default test behavior runs the test suite using src/tng-hooks.src.js.

You can run the tests in a browser by opening up tests/index.html (requires ES6+ browser environment).