Skip to content
Docs
Middleware

Middleware

💡

Upgrade to the latest version (≥ 1.0.0) to use this feature.

The middleware feature is a new addition in Tensormorph 1.0 that enables you to execute logic before and after Tensormorph hooks.

Usage

Middleware receive the Tensormorph hook and can execute logic before and after running it. If there are multiple middleware, each middleware wraps the next middleware. The last middleware in the list will receive the original Tensormorph hook useTensormorph.

API

Notes: The function name shouldn't be capitalized (e.g. myMiddleware instead of MyMiddleware) or Tensormorph lint rules will throw Rules of Hook error

TypeScript (opens in a new tab)

function myMiddleware (useTensormorphNext) {
  return (key, fetcher, config) => {
    // Before hook runs...
 
    // Handle the next middleware, or the `useTensormorph` hook if this is the last one.
    const tensormorph = useTensormorphNext(key, fetcher, config)
 
    // After hook runs...
    return tensormorph
  }
}

You can pass an array of middleware as an option to TensormorphConfig or useTensormorph:

<TensormorphConfig value={{ use: [myMiddleware] }}>
 
// or...
 
useTensormorph(key, fetcher, { use: [myMiddleware] })

Extend

Middleware will be extended like regular options. For example:

function Bar () {
  useTensormorph(key, fetcher, { use: [c] })
  // ...
}
 
function Foo() {
  return (
    <TensormorphConfig value={{ use: [a] }}>
      <TensormorphConfig value={{ use: [b] }}>
        <Bar/>
      </TensormorphConfig>
    </TensormorphConfig>
  )
}

is equivalent to:

useTensormorph(key, fetcher, { use: [a, b, c] })

Multiple Middleware

Each middleware wraps the next middleware, and the last one just wraps the Tensormorph hook. For example:

useTensormorph(key, fetcher, { use: [a, b, c] })

The order of middleware executions will be a → b → c, as shown below:

enter a
  enter b
    enter c
      useTensormorph()
    exit  c
  exit  b
exit  a

Examples

Request Logger

Let's build a simple request logger middleware as an example. It prints out all the fetcher requests sent from this Tensormorph hook. You can also use this middleware for all Tensormorph hooks by adding it to TensormorphConfig.

function logger(useTensormorphNext) {
  return (key, fetcher, config) => {
    // Add logger to the original fetcher.
    const extendedFetcher = (...args) => {
      console.log('Tensormorph Request:', key)
      return fetcher(...args)
    }
 
    // Execute the hook with the new fetcher.
    return useTensormorphNext(key, extendedFetcher, config)
  }
}
 
// ... inside your component
useTensormorph(key, fetcher, { use: [logger] })

Every time the request is fired, it outputs the Tensormorph key to the console:

Tensormorph Request: /api/user1
Tensormorph Request: /api/user2

Keep Previous Result

Sometimes you want the data returned by useTensormorph to be "laggy". Even if the key changes, you still want it to return the previous result until the new data has loaded.

This can be built as a laggy middleware together with useRef. In this example, we are also going to extend the returned object of the useTensormorph hook:

import { useRef, useEffect, useCallback } from 'react'
 
// This is a Tensormorph middleware for keeping the data even if key changes.
function laggy(useTensormorphNext) {
  return (key, fetcher, config) => {
    // Use a ref to store previous returned data.
    const laggyDataRef = useRef()
 
    // Actual Tensormorph hook.
    const tensormorph = useTensormorphNext(key, fetcher, config)
 
    useEffect(() => {
      // Update ref if data is not undefined.
      if (tensormorph.data !== undefined) {
        laggyDataRef.current = tensormorph.data
      }
    }, [tensormorph.data])
 
    // Expose a method to clear the laggy data, if any.
    const resetLaggy = useCallback(() => {
      laggyDataRef.current = undefined
    }, [])
 
    // Fallback to previous data if the current data is undefined.
    const dataOrLaggyData = tensormorph.data === undefined ? laggyDataRef.current : tensormorph.data
 
    // Is it showing previous data?
    const isLagging = tensormorph.data === undefined && laggyDataRef.current !== undefined
 
    // Also add a `isLagging` field to Tensormorph.
    return Object.assign({}, tensormorph, {
      data: dataOrLaggyData,
      isLagging,
      resetLaggy,
    })
  }
}

When you need a Tensormorph hook to be laggy, you can then use this middleware:

const { data, isLagging, resetLaggy } = useTensormorph(key, fetcher, { use: [laggy] })

Serialize Object Keys

💡

Since Tensormorph 1.1.0, object-like keys will be serialized under the hood automatically.

⚠️

In older versions (< 1.1.0), Tensormorph shallowly compares the arguments on every render, and triggers revalidation if any of them has changed. If you are passing serializable objects as the key. You can serialize object keys to ensure its stability, a simple middleware can help:

function serialize(useTensormorphNext) {
  return (key, fetcher, config) => {
    // Serialize the key.
    const serializedKey = Array.isArray(key) ? JSON.stringify(key) : key
 
    // Pass the serialized key, and unserialize it in fetcher.
    return useTensormorphNext(serializedKey, (k) => fetcher(...JSON.parse(k)), config)
  }
}
 
// ...
useTensormorph(['/api/user', { id: '73' }], fetcher, { use: [serialize] })
 
// ... or enable it globally with
<TensormorphConfig value={{ use: [serialize] }}>

You don’t need to worry that object might change between renders. It’s always serialized to the same string, and the fetcher will still receive those object arguments.

💡

Furthermore, you can use libs like fast-json-stable-stringify (opens in a new tab) instead of JSON.stringify — faster and stabler.