Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

moize

planttheidea695.5kMIT6.1.6TypeScript support: included

Blazing fast memoization based on all parameters passed

cache, expire, lru, memoize, memoization, optimize, performance, promise, ttl

readme

moize

moize is a consistently blazing fast memoization library for JavaScript. It handles multiple parameters (including default values) without any additional configuration, and offers a large number of options to satisfy any number of potential use-cases.

$ npm i moize --save

Importing

ESM in browsers

import moize from 'moize';

ESM in NodeJS

import moize from 'moize/mjs/index.mjs';

CommonJS

const moize = require('moize');

Usage

import moize from 'moize';

const method = (a: number, b: number) => a + b;

const memoized = moize(method);

memoized(2, 4); // 6
memoized(2, 4); // 6, pulled from cache

All parameter types are supported, including circular objects, functions, etc. There are also a number of shortcut methods to memoize for unique use-cases.

Configuration options

moize optionally accepts an object of options as either the second parameter or as the first step in a curried function:

// inline
moize(fn, options);

// curried
moize(options)(fn);

The full shape of these options:

type Options = {
    // is the cache based on deep equality of each key argument
    isDeepEqual: boolean;
    // is the result a promise
    isPromise: boolean;
    // is the result a React component
    isReact: boolean;
    // should the parameters be serialized instead of directly referenced
    isSerialized: boolean;
    // is the cache based on shallow equality of each key argument
    isShallowEqual: boolean;
    // custom method to compare equality between two key arguments
    matchesArg: (cachedKeyArg: any, keyArg: any) => boolean;
    // custom method to compare equality across all key arguments
    matchesKey: (cachedKey: any[], key: any[]) => boolean;
    // amount of time in milliseconds before the cache will expire
    maxAge: number;
    // maximum number of arguments passed to use as key for caching
    maxArgs: number;
    // maximum size of cache for this method
    maxSize: number;
    // method fired when a new entry is added to cache
    onCacheAdd: (
        cache: moize.Cache,
        options: moize.Options,
        moized: (...args: any[]) => any
    ) => void;
    // method fire when either a new entry is added to cache or the LRU ordering of the cache has changed
    onCacheChange: (
        cache: moize.Cache,
        options: moize.Options,
        moized: (...args: any[]) => any
    ) => void;
    // method fired when an existing entry in cache is used
    onCacheHit: (
        cache: moize.Cache,
        options: moize.Options,
        moized: (...args: any[]) => any
    ) => void;
    // method to fire when a cache entry expires (in combination with maxAge)
    onExpire: (key: any[]) => void;
    // the unique identifier to give the memoized method when collecting statistics
    profileName: string;
    // method to serialize the arguments to build a unique cache key
    serializer: (key: any[]) => string;
    // method to transform the args into a custom format for key storage in cache
    transformArgs: (key: any[]) => any[];
    // should the cache entry be refreshed by calling the underlying function with the same parameters and
    // updating the value stored in cache to be the new result
    updateCacheForKey: (key: any[]) => boolean;
    // should the cache entry's expiration be refreshed when the cache entry is hit (in combination with maxAge)
    updateExpire: boolean;
};

All default values can be found here.

isDeepEqual

defaults to false

Should deep equality be used to compare cache each key argument.

type Arg = {
    one: {
        nested: string;
    };
    two: string;
};

const fn = ({ one, two }: Arg) => [one, two];

const memoized = moize(fn, { isDeepEqual: true });

memoized({ one: { nested: 'one' }, two: 'two' });
memoized({ one: { nested: 'one' }, two: 'two' }); // pulls from cache

This is also available via the shortcut method of moize.deep

const memoized = moize.deep(fn);

isPromise

defaults to false

Is the computed value in the function a Promise.

const fn = async (item: Promise<string>) => await item;

const memoized = moize(fn, { isPromise: true });

This is also available via the shortcut method of moize.promise.

const memoized = -moize.promise(fn);

The Promise itself will be stored in cache, so that cached returns will always maintain the Promise contract. For common usage reasons, if the Promise is rejected, the cache entry will be deleted.

isReact

defaults to false

Is the function passed a stateless functional React component.

type Props = {
    one: string;
    two: number;
};

const Component = ({ one, two }: Props) => (
    <div>
        {one}: {two}
    </div>
);

const MemoizedFoo = moize(Component, { isReact: true });

This is also available via the shortcut method of moize.react.

const MemoizedFoo = moize.react(Component);

The method will do a shallow equal comparison of both props and legacy context of the component based on strict equality. If you want to do a deep equals comparison, set isDeepEqual to true.

NOTE: This will memoize on each instance of the component passed, which is equivalent to PureComponent or React.memo. If you want to memoize on all instances (which is how this option worked prior to version 6), use the following options:

const memoized = moize(Component, { isShallowEqual: true, maxArgs: 2 });

isSerialized

defaults to false

Serializes the parameters passed into a string and uses this as the key for cache comparison.

const fn = (mutableObject: { one: Record<string, any> }) =>
    mutableObject.property;

const memoized = moize(fn, { isSerialized: true });

This is also available via the shortcut method of moize.serialize.

const memoized = moize.serialize(fn);

If serialize is combined with either maxArgs or transformArgs, the following order is used:

  1. limit by maxArgs (if applicable)
  2. transform by transformArgs (if applicable)
  3. serialize by serializer

NOTE: This is much slower than the default key storage, and usually the same requirements can be meet with isDeepEqual, so use at your discretion.

isShallowEqual

defaults to false

Should shallow equality be used to compare cache each key argument.

type Arg = {
    one: string;
    two: string;
};

const fn = ({ one, two }: Arg) => [one, two];

const memoized = moize(fn, { isShallowEqual: true });

memoized({ one: 'one', two: 'two' });
memoized({ one: 'one', two: 'two' }); // pulls from cache

This is also available via the shortcut method of moize.shallow

const memoized = moize.shallow(fn);

matchesArg

defaults to SameValueZero equality

Custom method used to compare equality of keys for cache purposes by comparing each argument.

type Arg = {
    one: string;
    two: string;
};

const fn = ({ one, two }: Arg) => [one, two];

const hasOneProperty = (cacheKeyArg: Arg, keyArg: Arg) =>
    Object.keys(cacheKeyArg).length === 1 && Object.keys(keyArg).length === 1;

const memoized = moize(fn, { matchesArg: hasOneProperty });

memoized({ one: 'two' };
memoized({ two: 'three' }); // pulls from cache

This is also available via the shortcut method of moize.matchesArg

const memoized = moize.matchesArg(hasOneProperty)(fn);

NOTE: This comparison is used iteratively on each argument, rather than comparing the two keys as a whole. If you want to compare the key as a whole, you should use matchesKey.

matchesKey

Custom method used to compare equality of keys for cache purposes by comparing the entire key.

type Arg = {
    one: string;
    two: string;
};

const fn = ({ one, two }: Arg) => [one, two];

const isFooEqualAndHasBar = (cacheKey: [Arg], key: [Arg]) =>
    cacheKey[0].one === key[0].one &&
    cacheKey[1].hasOwnProperty('two') &&
    key[1].hasOwnProperty('two');

const memoized = moize(fn, { matchesKey: isFooEqualAndHasBar });

memoized({ one: 'two' }, { two: null });
memoized({ one: 'two' }, { two: 'three' }); // pulls from cache

This is also available via the shortcut method of moize.matchesKey

const memoized = moize.matchesKey(isFooEqualAndHasBar)(fn);

NOTE: This comparison uses the two keys as a whole, which is usually less performant than the matchArg comparison used iteratively on each argument. Generally speaking you should use the matchArg option for equality comparison.

maxAge

The maximum amount of time in milliseconds that you want a computed value to be stored in cache for this method.

const fn = (item: Record<string, any>) => item;

const MAX_AGE = 1000 * 60 * 5; // five minutes;

const memoized = moize(fn, { maxAge: MAX_AGE });

This is also available via the shortcut method of moize.maxAge.

const memoized = moize.maxAge(MAX_AGE)(fn);

TIP: A common usage of this is in tandom with isPromise for AJAX calls, and in that scenario the expected behavior is usually to have the maxAge countdown begin upon resolution of the promise. If this is your intended use case, you should also apply the updateExpire option.

maxArgs

The maximum number of arguments (starting from the first) used in creating the key for the cache.

const fn = (item1: string, item2: string, item3: string) =>
    item1 + item2 + item3;

const memoized = moize(fn, { maxArgs: 2 });

memoize('one', 'two', 'three');
memoize('one', 'two', 'four'); // pulls from cache, as the first two args are the same

This is also available via the shortcut method of moize.maxArgs.

const memoized = moize.maxArgs(2)(fn);

If maxArgs is combined with either serialize or transformArgs, the following order is used:

  1. limit by maxArgs
  2. transform by transformArgs (if applicable)
  3. serialize by serializer (if applicable)

maxSize

defaults to 1

The maximum number of values you want stored in cache for this method. Clearance of the cache once the maxSize is reached is on a Least Recently Used basis.

const fn = (item: string) => item;

const memoized = moize(fn, { maxSize: 5 });

This is also available via the shortcut method of moize.maxSize.

const memoized = moize.maxSize(5)(fn);

onCacheAdd

Method to fire when an item has been added to cache. Receives the cache, options, and memoized function as a parameters.

const fn = (one: string, two: string) => [one, two];

const logCacheKeys = (
    cache: Cache,
    options: Options,
    moized: Moized<typeof fn>
) => console.log(cache.keys);

const moized = moize(fn, { maxSize: 2, onCacheAdd: logCacheKeys });

moized('one', 'two'); // [["one","two"]]
moized('one', 'two');
moized('two', 'one'); // [["two","one"], ["one","two"]]
moized('one', 'two');

NOTE: When combined with onCacheChange, this method will always fire first.

onCacheChange

Method to fire when an item has been either added to cache, or existing cache was reordered based on a cache hit. Receives the cache, options, and memoized function as a parameters.

const fn = (one: string, two: string) => [one, two];

const logCacheKeys = (
    cache: Cache,
    options: Options,
    moized: Moized<typeof fn>
) => console.log(cache.keys);

const moized = moize(fn, { maxSize: 2, onCacheChange: logCacheKeys });

moized('one', 'two'); // [["one","two"]]
moized('one', 'two');
moized('two', 'one'); // [["two","one"], ["one","two"]]
moized('one', 'two'); // [["one","two"], ["two","one"]]

NOTE: When combined with onCacheAdd or onCacheHit, this method will always fire last.

onCacheHit

Method to fire when an existing cache item is found. Receives the cache, options, and memoized function as a parameters.

const fn = (one: string, two: string) => [one, two];

const logCacheKeys = (
    cache: Cache,
    options: Options,
    moized: Moized<typeof fn>
) => console.log(cache.keys);

const moized = moize(fn, { maxSize: 2, onCacheHit: logCacheKeys });

moized('one', 'two');
moized('one', 'two'); // [["one","two"]]
moized('two', 'one');
moized('one', 'two'); // [["two","one"], ["one","two"]]

NOTE: When combined with onCacheChange, this method will always fire first.

onExpire

A callback that is called when the cached entry expires.

const fn = (item: string) => item;

const logKey = (key: Key<string>) => console.log(key);

const memoized = moize(fn, { maxAge: 10000, onExpire: logKey });

If you return false from this method, it will prevent the key's removal and refresh the expiration in the same vein as updateExpire based on maxAge:

const fn = (item: string) => item;

let expirationAttempts = 0;

const limitExpirationAttempts = (key: Key<string>) => {
    expirationAttempts += 1;

    return expirationAttempts < 2;
};

const memoized = moize(fn, {
    maxAge: 10000,
    onExpire: limitExpirationAttempts,
});

memoized('one'); // will expire key after 30 seconds, or 3 expiration attempts

NOTE: You must set a maxAge for this option to take effect.

profileName

defaults to function name when it exists, or Anonymous {count} otherwise

Name to use as unique identifier for the function when collecting statistics.

moize.collectStats();

const fn = (item: string) => item;

const memoized = moize(fn, { profileName: 'my fancy identity' });

This is also available via the shortcut method of moize.profile.

const memoized = moize.profile('profile-name')(fn);

NOTE: You must be collecting statistics for this option to provide value, as it is the identifier used for statistics collection.

serializer

defaults to serializeArguments in utils.js

Method used in place of the internal serializer when serializing the parameters for cache key comparison. The function accepts a single argument, the Array of args, and must also return an Array.

const fn = (one: string, two: string) => [one, two];

const customSerializer = (args: string[]) => [JSON.stringify(args[0])];

const memoized = moize(fn, {
    isSerialized: true,
    serializer,
});

This is also available via the shortcut method of moize.serializeWith.

const memoized = moize.serializeWith(customSerializer)(fn);

NOTE: You must set isSerialized for this option to take effect.

transformArgs

Transform the arguments passed before it is used as a key. The function accepts a single argument, the Array of args, and must also return an Array.

const fn = (one: string | null, two: string | null, three: string | null) => [
    two,
    three,
];

const ignoreFirstArg = (args: (string | null)[]) => args.slice(1);

const moized = moize(fn, { transformArgs: ignoreFirstArg });

moize('one', 'two', 'three');
moize(null, 'two', 'three'); // pulled from cache

This is also available via the shortcut method of moize.transformArgs.

const memoized = moize.transformArgs(argTransformer)(fn);

If transformArgs is combined with either maxArgs or serialize, the following order is used:

  1. limit by maxArgs (if applicable)
  2. transform by transformArgs
  3. serialize by serializer (if applicable)

updateCacheForKey

If you want to update the cache for a given key instead of leverage the value currently stored in cache.

const fn = (item: string) => item;

let lastUpdate = Date.now();

const memoized = moize(fn, {
    updateCacheForKey([item]: [string]) {
        const now = Date.now();
        const last = lastUpdated;

        lastUpdate = now;

        // its been more than 5 minutes since last update
        return last + 300000 < now;
    },
});

memoized('one');
memoized('one'); // pulled from cache

// 5 minutes later

memoized('one'); // re-calls method and updates cache

This is also available via the shortcut method of moize.updateCacheForKey.

const memoized = moize.updateCacheForKey(shouldCacheUpdate)(fn);

updateExpire

When a maxAge is set, clear the scheduled expiration of the key when that key is retrieved, setting a new expiration based on the most recent retrieval from cache.

const fn = (item: string) => item;

const MAX_AGE = 1000 * 60 * 5; // five minutes

const memoized = moize(fn, { maxAge: MAX_AGE, updateExpire: true });

memoized('one');

setTimeout(() => {
    /**
     * hits cache, which updates the expire to be 5 minutes
     * from this run instead of the first
     */
    memoized('one');
}, 1000 * 60);

Usage with shortcut methods

moize.deep

Pre-applies the isDeepEqual option.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.deep(fn);

moize.infinite

Pre-applies the maxSize option with Infinity.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.infinite(fn);

NOTE: This mimics default behavior of moize prior to version 6.

moize.matchesArg

Pre-applies the matchesArg option as a curriable method.

import moize from 'moize';

const isEqualOrFoo = (cacheKeyArg: string, keyArg: string) =>
    cacheKeyArg === keyArg || keyArg === 'one';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.matchesArg(isEqualOrFoo)(fn);

moize.matchesKey

Pre-applies the matchesKey option as a curriable method.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

const isEqualOrHasFoo = (cacheKey: Key<string>, key: Key<string>) =>
    key.every((keyArg, index) => keyArg === cacheKey[index]) ||
    key.some((keyArg) => keyArg === 'one');

export default moize.matchesKey(isEqualOrHasFoo)(fn);

moize.maxAge

Pre-applies the maxAge option as a curriable method.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.maxAge(5000)(fn);

moize.maxArgs

Pre-applies the maxArgs option as a curriable method.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.maxArgs(1)(fn);

moize.maxSize

Pre-applies the maxSize option as a curriable method.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.maxSize(5)(fn);

moize.profile

Pre-applies the profileName option as a curriable method.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.profile('my fancy identity')(fn);

NOTE: You must be collecting statistics for this option to provide value, as it is the identifier used for statistics collection.

moize.promise

Pre-applies the isPromise and updateExpire options. The updateExpire option does nothing if maxAge is not also applied, but ensures that the expiration begins at the resolution of the promise rather than the instantiation of it.

import moize from 'moize';

const fn = async (one: string, two: Record<string, any>) =>
    await someApiCall(one, two);

export default moize.promise(fn);

NOTE: If you do not want the promise to update its expiration when the cache is hit, then you should use the isPromise option directly instead.

moize.react

Pre-applies the isReact) option for memoizing functional components in React. Key comparisons are based on a shallow equal comparison of both props and legacy context.

import moize from 'moize';

type Props = {
    one: string;
    two: number;
};

const Component = ({ one, two }: Props) => (
    <div>
        {one} {two}
    </div>
);

export default moize.react(Component);

NOTE: This method will not operate with components made via the class instantiation, as they do not offer the same referential transparency.

moize.serialize

Pre-applies the isSerialized option.

import moize from 'moize';

const fn = (one: Record<string, any>, two: Record<string, any>) => ({
    one,
    two,
});

export default moize.serialize(fn);

NOTE: If you want to provide a custom serializer, you should use moize.serializeWith:

moize.serializeWith(customSerializer)(fn);

moize.serializeWith

Pre-applies the isSerialized and serializer options.

import moize from 'moize';

const fn = (one: Record<string, any>, two: Record<string, any>) => ({
    one,
    two,
});

export default moize.serializeWith(JSON.stringify)(fn);

NOTE: If you want to use the default serializer, you should use moize.serialize:

moize.serialize(customSerializer)(fn);

moize.shallow

Pre-applies the isShallowEqual option.

import moize from 'moize';

const fn = (one: string, two: string) => `${one} ${two}`;

export default moize.shallow(fn);

moize.transformArgs

Pre-applies the transformArgs option.

import moize from 'moize';

const fn = ([one, two]: string[]) => [`${one} ${two}`];

export default moize.transformArgs(fn);

moize.updateCacheForKey

Pre-applies the updateCacheForKey option.

import moize from 'moize';

let lastUpdated = Date.now();

const fn = () => {
    const now = Date.now();
    const last = lastUpdated;

    lastUpdate = now;

    // its been more than 5 minutes since last update
    return last + 300000 < now;
};

export default moize.updateCacheForKey(fn);

useMoize hook

If you are using React 16.8+ and are using hooks, you can easily create a custom useMoize hook for your project:

import { useRef } from 'react';

export function useMoize(fn, args, options) {
    const moizedFnRef = useRef(moize(fn, options));

    return moizedFnRef.current(...args);
}

Which can then be used as such:

import React from 'react';

import { useMoize } from './moize-hooks';

function MyComponent({ first, second, object }) {
    // standard usage
    const sum = useMoize((a, b) => a + b, [first, second]);
    // with options
    const deepSum = useMoize((obj) => obj.a + obj.b, [object], {
        isDeepEqual: true,
    });

    return (
        <div>
            Sum of {first} and {second} is {sum}. Sum of {object.a} and{' '}
            {object.b} is {deepSum}.
        </div>
    );
}

Naturally you can tweak as needed for your project (default options, option-specific hooks, etc).

NOTE: This is very similar to useCallback built-in hook, with two main differences:

  • There is a third parameter passed (the options passed to moize)
  • The second argument array is the list of arguments passed to the memoized function

In both useCallback and useMemo, the array is a list of dependencies which determine whether the funciton is called. These can be different than the arguments, although in general practice they are equivalent. The decision to use them directly was both for this common use-case reasons, but also because the implementation complexity would have increased substantially if not.

Composition

Starting with version 2.3.0, you can compose moize methods. This will create a new memoized method with the original function that shallowly merges the options of the two setups. Example:

import moize from 'moize';

const Component = (props: Record<string, any>) => <div {...props} />;

// memoizing with react, as since 2.0.0
const MemoizedFoo = moize.react(Component);

// creating a separately-memoized method that has maxSize of 5
const LastFiveFoo = moize.maxSize(5)(MemoizedFoo);

You can also create an options-first curriable version of moize if you only pass the options:

import moize from 'moize';

// creates a function that will memoize what is passed
const limitedSerializedMoize = moize({ maxSize: 5, serialize: true });

const getWord = (bird) => `${bird} is the word`;

const moizedGetWord = limitedSerializedMoize(getWord);

You can also combine all of these options with moize.compose to create moize wrappers with pre-defined options.

import moize from 'moize';

// creates a moizer that will have the options of
// {isReact: true, maxAge: 5000, maxSize: 5}
const superLimitedReactMoize = moize.compose(
    moize.react,
    moize.maxSize(5),
    moize.maxAge(5000)
);

Collecting statistics

As-of version 5, you can collect statistics of moize to determine if your cached methods are effective.

import moize from 'moize';

moize.collectStats();

const fn = (one: string, two: string) => [one, two];

const moized = moize(fn);

moized('one', 'two');
moized('one', 'two');

moized.getStats(); // {"calls": 2, "hits": 1, "usage": "50%"}

NOTE: It is recommended not to activate this in production, as it will have a performance decrease.

Stats methods

clearStats

Cear statistics on moized functions.

moize.clearStats(); // clears all stats
moize.clearStats('profile-name'); // clears stats only for 'profile-name'

collectStats

Set whether collecting statistics on moized functions.

moize.collectStats(true); // start collecting stats
moize.collectStats(); // same as passing true
moize.collectStats(false); // stop collecting stats

NOTE: If collecting statistics, it is recommended to provide a custom profileName or use moize.profile() for all memoized functions. This allows easier mapping of resulting statistics to their origin function when it has a common name or is anonymous.

getStats([profileName])

Get the statistics for a specific function, or globally.

moize.collectStats();

const fn = (one: string, two: string) => [one, two];

const moized = moize(fn);

const otherFn = (one: string[]) => one.slice(0, 1);

const otherMoized = moize(otherFn, { profileName: 'otherMoized' });

moized('one', 'two');
moized('one', 'two');

moized.getStats(); // {"calls": 2, "hits": 1, "usage": "50%"}

otherMoized(['three']);

moize.getStats('otherMoized'); // {"calls": 1, "hits": 0, "usage": "0%"}

moize.getStats();
/*
 {
   "calls": 3,
   "hits": 1,
   "profiles": {
     "fn at Object..src/utils.js (http://localhost:3000/app.js:153:68)": {
       "calls": 2,
       "hits": 1,
       "usage": "50%"
     },
     "otherMoized": {
       "calls": 1,
       "hits": 0,
       "usage": "0%"
     }
   },
   "usage": "33.3333%"
 }
 */

Introspection

isCollectingStats

Are statistics being collected on memoization usage.

moize.collectStats(true);
moize.isCollectingStats(); // true
moize.collectStats(false);
moize.isCollectingStats(); // false

isMoized

Is the function passed a moized function.

const fn = () => {};
const moizedFn = moize(fn);

moize.isMoized(fn); // false
moize.isMoized(moizedFn); // true

Direct cache manipulation

The cache is available on the moized function as a property, and while it is not recommended to modify it directly, that option is available for edge cases.

cache

The shape of the cache is as follows:

type Cache = {
    keys: any[][];
    size: number;
    values: any[];
};

Regardless of how the key is transformed, it is always stored as an array (if the value returned is not an array, it is coalesced to one).

NOTE: The order of keys and values should always align, so be aware when manually manipulating the cache that you need to manually keep in sync any changes to those arrays.

cacheSnapshot

The cache is mutated internally for performance reasons, so logging out the cache at a specific step in the workflow may not give you the information you need. As such, to help with debugging you can request the cacheSnapshot, which has the same shape as the cache but is a shallow clone of each property for persistence.

There are also convenience methods provided on the moized function which allow for programmatic manipulation of the cache.

add(key, value)

This will manually add the value at key in cache if key does not already exist. key should be an Array of values, meant to reflect the arguments passed to the method.

// single parameter is straightforward
const memoized = moize((item: string) => item: string);

memoized.add(['one'], 'two');

// pulls from cache
memoized('one');

NOTE: This will only add keys that do not exist in the cache, and will do nothing if the key already exists. If you want to update keys that already exist, use update.

clear()

This will clear all values in the cache, resetting it to an empty state.

const memoized = moize((item: string) => item);

memoized.clear();

get(key)

Returns the value in cache if the key matches, else returns undefined. key should be an Array of values, meant to reflect the arguments passed to the method.

const memoized = moize((one: string, two: string) => [one, two);

memoized('one', 'two');

console.log(memoized.get(['one', 'two'])); // ["one","two"]
console.log(memoized.get(['two', 'three'])); // undefined

getStats()

Returns the statistics for the function.

moize.collectStats();

const memoized = moize((one: string, two: string) => [one, two);

memoized('one', 'two');
memoized('one', 'two');

console.log(memoized.getStats()); // {"calls": 2, "hits": 1, "usage": "50%"}

NOTE: You must be collecting statistics for this to be populated.

has(key)

This will return true if a cache entry exists for the key passed, else will return false. key should be an Array of values, meant to reflect the arguments passed to the method.

const memoized = moize((one: string, two: string) => [one, two]);

memoized('one', 'two');

console.log(memoized.has(['one', 'two'])); // true
console.log(memoized.has(['two', 'three'])); // false

keys()

This will return a list of the current keys in cache.

const memoized = moize.maxSize(2)((item: any) => item);

memoized('one');
memoized({ two: 'three' });

const keys = memoized.keys(); // [['one'], [{two: 'three'}]]

remove(key)

This will remove the provided key from cache. key should be an Array of values, meant to reflect the arguments passed to the method.

const memoized = moize((item: { one: string }) => item);

const arg = { one: 'one' };

memoized(arg);

memoized.remove([arg]);

// will re-execute, as it is no longer in cache
memoized(arg);

NOTE: This will only remove keys that exist in the cache, and will do nothing if the key does not exist.

update(key, value)

This will manually update the value at key in cache if key exists. key should be an Array of values, meant to reflect the arguments passed to the method.

// single parameter is straightforward
const memoized = moize((item: string) => item);

memoized.add(['one'], 'two');

// pulls from cache
memoized('one');

NOTE: This will only update keys that exist in the cache, and will do nothing if the key does not exist. If you want to add keys that do not already exist, use add.

values()

This will return a list of the current values in cache.

const memoized = moize.maxSize(2)((item: string | { two: string }) => ({
    item,
}));

memoized('one');
memoized({ two: 'three' });

const values = memoized.values(); // [{item: 'one'}, {item: {two: 'three'}}]

Benchmarks

All values provided are the number of operations per second calculated by the Benchmark suite, where a higher value is better. Each benchmark was performed using the default configuration of the library, with a fibonacci calculation based on a starting parameter of 35, using single and multiple parameters with different object types. The results were averaged to determine overall speed across possible usage.

NOTE: lodash, ramda, and underscore do not support multiple-parameter memoization without use of a resolver function. For consistency in comparison, each use the same resolver that returns the result of JSON.stringify on the arguments.

Name Overall (average) Single (average) Multiple (average) single primitive single array single object multiple primitive multiple array multiple object
moize 71,177,801 98,393,482 43,962,121 139,808,786 97,571,202 57,800,460 44,509,528 44,526,039 42,850,796
lru-memoize 48,391,839 64,270,849 32,512,830 77,863,436 59,876,764 55,072,348 29,917,027 33,308,028 34,313,435
mem 42,348,320 83,158,473 1,538,166 128,731,510 73,473,478 47,270,433 2,012,120 1,565,253 1,037,126
fast-memoize 33,145,713 64,942,152 1,349,274 190,677,799 2,149,467 1,999,192 1,718,229 1,297,911 1,031,683
lodash 25,700,293 49,941,573 1,459,013 67,513,655 48,874,559 33,436,506 1,861,982 1,402,532 1,112,527
memoizee 21,546,499 27,447,855 15,645,143 29,701,124 27,294,197 25,348,244 15,359,792 15,855,421 15,720,217
ramda 18,804,380 35,919,033 1,689,727 101,557,928 1,895,956 4,303,215 2,305,025 1,597,131 1,167,025
memoizerific 6,745,058 7,382,030 6,108,086 8,488,885 6,427,832 7,229,375 5,772,461 6,278,344 6,273,453
underscore 6,701,695 11,698,265 1,705,126 18,249,423 4,695,658 12,149,714 2,310,412 1,630,769 1,174,197
addy-osmani 4,926,732 6,370,152 3,483,311 12,506,809 3,568,399 3,035,249 6,898,542 2,009,089 1,542,304

Filesize

moize is fairly small (~3.86KB when minified and gzipped), however it provides a large number of configuration options to satisfy a number of edge cases. If filesize is a concern, you may consider using micro-memoize. This is the memoization library that powers moize under-the-hood, and will handle most common use cases at 1/4 the size of moize.

Browser support

  • Chrome (all versions)
  • Firefox (all versions)
  • Edge (all versions)
  • Opera 15+
  • IE 9+
  • Safari 6+
  • iOS 8+
  • Android 4+

Development

Standard stuff, clone the repo and npm install dependencies. The npm scripts available:

  • benchmark => run the benchmark suite pitting moize against other libraries in common use-cases
  • benchmark:alternative => run the benchmark suite for alternative forms of caching
  • benchmark:array => run the benchmark suite for memoized methods using single and multiple array parameters
  • benchmark:object => run the benchmark suite for memoized methods using single and multiple object parameters
  • benchmark:primitive => run the benchmark suite for memoized methods using single and multiple object parameters
  • benchmark:react => run the benchmark suite for memoized React components
  • build => run rollup to build the distributed files in dist
  • clean:dist => run rimraf on the dist folder
  • clean:docs => run rimraf on the docs folder
  • clean:mjs => run rimraf on the mjs folder
  • copy:mjs => run clean:mjs and the es-to-mjs script
  • copy:types => copy internal types to be available for consumer
  • dev => run webpack dev server to run example app (playground!)
  • dist => runs clean:dist and build
  • docs => runs clean:docs and builds the docs via jsdoc
  • flow => runs flow check on the files in src
  • lint => runs ESLint against all files in the src folder
  • lint:fix => runs lint, fixing any errors if possible
  • test => run jest test functions with NODE_ENV=test
  • test:coverage => run test but with code coverage
  • test:watch => run test, but with persistent watcher
  • typecheck => run tsc against source code to validate TypeScript

changelog

moize CHANGELOG

6.1.6

  • #196 - Fix mjs typing not flowing through due to deep-linking ignoring index.d.ts

6.1.5

  • #186 - Avoid type issues when using in project where React is unused

6.1.4

  • #184 - Fix pass-through typing for micro-memoize dependency

6.1.3

  • #179 - Invalid types for namespace methods which give the impression that chaining of those methods is possible

6.1.2

  • #176 - Remove use of new Error().stack in derivation of fallback profileName, due to potential for OOM crashes in specific scenarios

6.1.1

  • Update fast-equals to latest major version

6.1.0

  • #161 - Provide custom function name when supported

6.0.3

  • #153 - Fix ESM/CommonJS cross-compatibility issues in NextJS

6.0.2

  • Update dependencies to latest (fast-equals in particular to prevent surfacing of an issue it faced)

6.0.1

  • #146 - Fix reading deprecated properties on function object

6.0.0

BREAKING CHANGES

  • moize v5 and below had a default cache size of Infinity, however starting with v6 the default cache size will be 1. If you want to maintain a cache history of multiple entries, it is recommended to use moize.maxSize to set the size of history desired. To replicate usage from v5 and below, use moize.infinite.
  • isReact option and moize.react shorthand method now memoizes on a per-instance basis (previously shared cache across all instances).
  • moize.simple and moize.reactSimple have been removed due to default cache size being 1.
  • isSerialized option and moize.serialize shorthand method will now serialize functions by default.
  • shouldSerializeFunctions option has been removed, as serializing functions is now the default.
  • equals option has been renamed to matchesArg for alignment with matchesKey.

ENHANCEMENTS

  • New options:
    • isShallowEqual (compares each arg in the key based on shallow equality)
    • updateCacheForKey (if truthy value returned, will update the cached value for the given key)
  • New shorthand methods:
    • moize.infinite (shorthand for maxSize option set to Infinity)
    • moize.matchesArg (shorthand for matchesArg option)
    • moize.matchesKey (shorthand for matchesKey option)
    • moize.serializeWith (shorthand for isSerialized and serializer option)
    • moize.shallow (shorthand for isShallowEqual option)
    • moize.transformArgs (shorthand for transformArgs option)
    • moize.updateCacheForKey (shorthand for updateCacheForKey option)

INTERNALS

  • Rewritten in TypeScript, with much better type declarations.
  • Changed unit test framework to jest
  • Used latest external dependencies for better ESM support in Node 14

5.4.7

  • Fix ESM usage in NodeJS 14 #128 (thanks @tozz)

5.4.6

  • Fix issue where setTimeout causes processes to hang in NodeJS #122

5.4.5

  • Improve TypeScript typings

5.4.4

  • Fix export in TypeScript declarations(#105)

5.4.3

  • Improve typings for TypeScript (#103)

5.4.2

  • Ignore when maxArgs or maxSize arguments are negative

5.4.1

  • Upgrade to babel@7
  • Add "sideEffects": false to package.json for better tree-shaking in webpack

5.4.0

5.3.2

BUGFIXES

  • Bump micro-memoize dependency to fix recursive race condition issue

5.3.1

BUGFIXES

  • Clean up logic surrounding update function to not unnecessarily assign the value twice

5.3.0

NEW FEATURES

5.2.3

BUGFIXES

  • Actually fix the issue that 5.2.2 was supposed to fix (appreciate the persistence @vlad-zhukov)

5.2.2

BUGFIXES

  • Fix issue where manually removing a key from cache would not clear / remove the expiration timeout

5.2.1

BUGFIXES

  • Fix Object.assign usage on functions (derp)

5.2.0

NEW FEATURES

  • Add collectStats as a convenience method on the main moize object

BUGFIXES

  • Fix naming convention for determining if statistics are being collected on the moized function (isCollectingStats now, was mistakenly collectStats before)

5.1.2

BUGFIXES

  • Reference the correct option name for micro-memoize (isMatchingKey) instead of moize (matchesKey) for instance methods (oops)

5.1.1

BUGFIXES

  • Ensure instance methods that update the cache work correctly with new matchesKey option

5.1.0

NEW FEATURES

  • Add matchesKey option
  • Change isSerialized to use matchesKey with custom isMatchingKey instead of standard isEqual

BUGFIXES

  • Prevent isDeepEqual from being passed to the micro-memoize options

5.0.1

  • Include fast-equals as an explicit dependency

5.0.0

  • Complete refactor to leverage micro-memoize under the hood (doubles the speed, and allows for more functionality)

BREAKING CHANGES

  • The default equals option now uses SameValueZero equality instead of strict equality
  • equals option is now performed iteratively on each argument in the key, rather on the key as a whole
    • matchesKey option now performs the full-key comparison that equals did in v4
  • serialize option has been changed to isSerialized
  • serializeFunctions option has been changed to shouldSerializeFunctions
  • CommonJS consumers must now use require('moize').default
  • The shape of the cache object has changed
    • If you were modifying it directly instead of using the convenience methods, you may experience breakages
  • moize.promise now sets both isPromise and updateExpire options (v4 and below only set isPromise)
  • When isPromise is true and a maxAge is applied, the maxAge now begins when the function is created rather than the resolution of the promise
    • If you want the maxAge to apply upon resolution, then either set the updateExpire option or use the moize.promise convenience method

NEW FEATURES

  • Deep equality now available without custom equals function (either isDeepEqual option, or moize.deep convenience method)
  • Statistics can now be collected for both global and method-specific contexts (see Collecting statistics and profileName)
  • Statistics-based introspection methods exist to support reading stats
    • getStats will get the stats for the profileName passed, or if none is passed then gets the global stats
    • isCollectingStats returns whether stats are being collected
  • Cache change listeners are now available to listen for changes
    • onCacheAdd will fire when a new entry is added to the cache
    • onCacheChange will fire when the cache is added to or the LRU order has changed
    • onCacheHit will fire when an existing entry in cache is used

BUGFIXES

  • isPromise methods that resolved had the values re-wrapped in a new promise, which obfuscated chaining on the method (now the original promise is maintained)
  • Internal de-circularizer for serialized methods was flaky in deeply-nested scenarios (now uses json-stringify-safe)

DEPRECATIONS

  • promiseLibrary is no longer a supported option (it is no longer needed, as the original promise is now maintained)

4.0.4

  • Add default property to moize which points to itself, allowing TypeScript to import correctly as an ES module

4.0.3

  • Fix issue where items manually added to cache did not have the same options (maxAge, maxSize, etc.) applied to them as those added to cache through function call

4.0.2

  • Remove duplicate internal method

4.0.1

  • Update issue where onExpire was firing prior to removal of the key from cache

4.0.0

  • Add programmatic update of expiration based on return from onExpire

BREAKING CHANGES

  • onExpire will update the expiration if false is returned
    • In prior versions, the return of onExpire was ignored entirely

3.5.0

  • Add updateExpire option, which if true will reset the expiration countdown based on maxAge when a cached value for that key has been hit
  • Update TypeScript typings (thanks @Joshuaweiss)

3.4.2

  • Remove unneeded constants declarations for smaller footprint

3.4.1

  • Remove code duplication and unnecessary cache key segregation (reduced library size by ~10.6%)
  • Improve Flow typings (no longer driven off of TypeScript typings)

3.4.0

  • Add onExpire callback that fires when a cache item expires

3.3.1

  • Fix TypeScript definitions with correct declarations (thanks @iHaiduk)

3.3.0

  • Add es transpilation in addition to standard lib, preserving ES2015 modules for pkg.module

3.2.2

  • Refactor ReactCacheKey to double the speed (yes, double)

3.2.1

  • Fix issue where MultipleParameterCacheKey and ReactCacheKey were not applying equals correctly

3.2.0

  • Add transformArgs option, which allows trasformation of the arguments prior to being used as a key

3.1.2

  • Fix clear referencing incorrect object
  • Fix equals not using matchesCustom when used in conjunction with serialize

3.1.1

  • BAD PUBLISH - do not use!

3.1.0

  • New: add isMoized introspection method
  • New; add FlowType declarations based on TypeScript typings
  • New: add prettier to project, with pre-commit hooks to format the code
  • Fix: only serialize key once when attempting to match in cache (was serializing on every iteration)

3.0.2

  • Improve performance of take by making it a higher-order function

3.0.1

  • Update TypeScript typings to include new equals option

3.0.0

  • Improve performance of multiple parameter cache matching (~3x faster)
  • Improve performance of react functional component cache matching (~9.5x faster)
  • Improve performance of serialized parameter cache matching (~1.5x faster)
  • Improve performance of use with maxArgs
  • Add equals option for ability to provide custom equality comparison method
  • Add moize.reactSimple shortcut method to limit react cache size to 1 (mimics the PureComponent optimization)
  • Add isReact option for simpler react configuration via options
  • Fix issue where moize was only able to curry options once
  • Fix issue with react cache where different functions with identical names / body contents were seen as equal
  • Fix issue where maxArgs was not always respected for serialize caches

BREAKING CHANGES

  • Custom cache is no longer available in options
  • moize.react now performs a shallow equal comparison of props and context instead of deep value comparison
    • If you want to perform a deep value equality comparison (if you are mutation props, for example), pass a deep equality comparison method via the equals option such as lodash's isEqual
    • If you want to continue using the v2 version of moize.react, you can manually apply the options: moize.serialize(fn, {maxArgs: 2, serializeFunctions: true})
  • The direct cache manipulation delete method has been renamed to remove
  • The direct cache manipulation hasCacheFor method has been renamed to has
  • The key passed to direct cache manipulation methods (add, has, remove) must now be an array
    • The array reflects the arguments passed to the method (moized.hasCacheFor('foo', 'bar') => moized.has(['foo', 'bar']))

2.5.1

  • Surface types for TypeScript correctly

2.5.0

  • Add TypeScript definitions (thanks vhfmag)
  • Skip unneeded first entry iteration when getting the multi-parameter key or finding the index of a key

2.4.1

  • Make positive integer checker use regex instead of bitwise operation

2.4.0

  • Add hasCacheFor method to determine if the memoized function has cache for given arguments

2.3.3

  • Remove unneeded iterator key generator method
  • Various micro-optimizations

2.3.2

  • Add lodash-webpack-plugin for smaller dist builds

2.3.1

  • Streamline the creation of curriable shortcuts (moize.maxAge, moize.maxSize)
  • Add curriable shortcut for moize.maxArgs (missed in initial release)

2.3.0

  • More options have shortcut methods
    • maxAge
      • Curried method (example: moize.maxAge(5000)(method))
    • maxSize
      • Curried method (example: moize.maxSize(5)(method))
    • promise (shortcut for isPromise: true)
    • serialize
    • simple (shortcut for maxSize: 1)
  • moize functions are now composable (example usage: moize.compose(moize.react, moize.simple))

2.2.3

  • Simplify internal vs custom cache recognition
  • Typing and documentation cleanup

2.2.2

  • Abstract out promiseResolver and promiseRejecter into separate testable functions
  • Various code cleanups and typing enhancements

2.2.1

  • Fix issue with delete checking size of the Cache before it had actually updated

2.2.0

  • Added promiseLibrary option to allow use of custom promise implementation
  • Bolster isPromise logic, auto-removing from cache when the promise is rejected
  • Update README for more detailed information on both isPromise and promiseLibrary options
  • Convert Cache key iteration to use custom iterator instead of standard loop for more stable iteration

2.1.6

  • Code cleanup

2.1.5

  • Fix issue where delete would always set the lastItem to undefined even when items still remained in cache

2.1.4

  • Move multiple-parameter key matching to cache (allows for custom cache to have its own implementation)
  • Update documentation for the custom cache implementation, as it requires getMultiParamKey now, plus give a better example of an alternative cache
  • Make keys() and values() methods no-ops when cache implementation is custom
  • Make deleteItemFromCache method safe with custom cache implementations

2.1.3

  • Enhance Cache more for multiple-parameter functions (now benchmarks faster in all categories)

2.1.2

  • Have areArraysShallowEqual use isEqual instead of strict equality to allow for NaN as key

2.1.1

  • Optimize cache class to be more efficient in both has and get operations
  • Fix issue with delete that was preventing delete of falsy keys

2.1.0

  • Add add method on cache to allow for manual cache insertion

2.0.3

  • Ensure maxArgs is respected for unserialized functions
  • Change the arguments length check from === 1 to > so that zero arguments (undefined key) pulls from cache directly instead of tries to go through key matching for multiparam

2.0.2

  • Prevent memoization on moize.react based on third parameter to functional components (object of all internal react functions)

2.0.1

  • Fix static types applied to functional components not being applied to memoized component

2.0.0

  • Refactor to use object equality instead of serialization (vast speed improvements over 1.x.x with multiple parameters)

BREAKING CHANGES

  • If you were relying on the serialization (using value equality instead of object equality), it will no longer memoize (you can set serialize: true if you want to continue using that option)
  • If you were using moize to memoize React components, you should change your invocations from moize to moize.react (see README)

1.5.0

  • Add values method on memoized function (gets list of computed values stored in cache)
  • Fix issue with clear method not being present on Cache

1.4.5

  • Switch to using Cache for caching instead of native Map (was previously only used for polyfilling, but is twice as fast ... I feel silly for not testing its performance prior)
  • Simplify and optimize Cache to crank out as much speed as possible

1.4.4

  • Add displayName property to memoized function for better display of memoized react components in DevTools
  • Throw a TypeError when the parameter passed to moize is not a function

1.4.3

  • Move internal serializer generation to utils, for further partial application leveraging

1.4.2

  • Leverage partial functions in a number of places (less arguments passed around, improves performance by ~6%)

1.4.1

  • Correct README error explaining serializeFunctions option

1.4.0

  • Add serializeFunctions option

1.3.3

  • Ensure all numeric parameters (maxAge, maxArgs, maxSize) are a finite positive integer

1.3.2

  • Replace array-based decycle with Map (performance on circular objects)

1.3.1

  • Fix README errors

1.3.0

  • Add keys method to memoized function, to know the size and also get keys for potential deletion
  • Update decycle method with modern techniques and helper functions for better circular reference performance

1.2.0

  • Add maxArgs configuration option to limit the number of arguments to use in the key creation for cache

1.1.2

  • Remove external dependencies in favor of local Cache and decycle implementation (smaller bundle size)

1.1.1

  • Make handling of circular handling automatic by stringifying in try / catch, and remove manual isCircular configuration option

1.1.0

  • Add cycle dependency to handle circular objects
  • Add clear and delete methods on the memoized function to allow for direct cache manipulation

1.0.3

  • Remove warning related to map-or-similar dependency consumption

1.0.2

  • Remove no-longer-needed dependencies

1.0.1

  • Remove unneeded folders and files from npm package

1.0.0

  • Initial release