listate

Library for listening on changes of Redux store state.

Usage no npm install needed!

<script type="module">
  import listate from 'https://cdn.skypack.dev/listate';
</script>

README

listate

Library to listen/observe/watch changes of Redux store state.

NPM version Build Status

Table of contents

Installation

Node

npm install listate

Bower

bower install listate

AMD, <script>

Use dist/listate.js or dist/listate.min.js (minified version). Use dist/extra.js or dist/extra.min.js (minified version) to apply extra functions.

Usage

ECMAScript 6

import listen from 'listate';
// Or if you need extra functionality
import extListen from 'listate/extra';

Node

const listen = require('listate').listen;
// Or if you need extra functionality
const extListen = require('listate/extra').listen;

AMD

define(['path/to/dist/listate.js', 'path/to/dist/extra.js'], function(listate, extra) {
    const listen = listate.listen;
    // Import extra.js if you need extra functionality
    const extListen = extra.listen;
});

Bower, <script>

<!-- Use bower_components/listate/dist/listate.js and bower_components/listate/dist/extra.js if the library was installed by Bower -->
<script type="text/javascript" src="path/to/dist/listate.js"></script>
<!-- Or if you need extra functionality -->
<script type="text/javascript" src="path/to/dist/extra.js"></script>
<script type="text/javascript">
    // listate is available via listate field of window object
    const listen = listate.listen;
    // Extra functionality is available inside extra namespace
    const extListen = listate.extra.listen;
</script>

Examples

import { createStore } from 'redux';
import listen from 'listate';
import extListen from 'listate/extra';

const initState = {
    user: null,
    section: '',
    map: {
        main: {}
    }
};

function reducer(state, action) {
    const { payload } = action;
    let newState;
    switch (action.type) {
        case 'AUTH':
            return Object.assign({}, state, {user: payload});
        case 'SELECT_SECTION':
            return Object.assign({}, state, {section: payload});
        case 'SET_SECTION':
            newState = Object.assign({}, state);
            newState.map = Object.assign({}, state.map);
            newState.map[payload.key] = payload.value;
            return newState;
        default:
            return state;
    }
}

const store = createStore(reducer, initState);

listen(store, {
    data: 'main',
    filter: (state) => state.user,
    // One-time listener
    once: true,
    handle(data) {
        // Dispatch any action
        data.dispatch({
            type: 'SELECT_SECTION',
            // data.current === state.user, data.data === 'main'
            payload: data.current.favoriteSection || localStorage.getItem('selectedSection') || data.data
        });
    }
});
listen(store, {
    filter: (state) => state.section,
    when: (current, prev) => current !== prev && current !== 'exit',
    // Call the listener no more frequently than once per second
    delay: 1000,
    handle(data) {
        // data.current === state.section
        localStorage.setItem('selectedSection', data.current);
        console.log('Saved section: ', data.current);
    }
});
listen(store, {
    description: 'map change listener',
    context: true,
    filter: (state) => state.map,
    when: (current, prev, data) => current.stat && data.state.user && data.state.section === 'video',
    handle(data) {
        console.log('data.prev:', data.prev);   // {main: {}}
        console.log('data.current:', data.current);   // {main: {}, stat: {a: 1}}
        console.log('this.description:', this.description);   // map change listener
    }
});
extListen(store, {
    filter: {s: 'section', main: 'map.main'},
    handle(data) {
        console.log('extListen: data.prev -', data.prev);
        console.log('extListen: data.current -', data.current);
    }
});
...
store.dispatch({
    type: 'AUTH',
    payload: {login: 'commander'}
});
...
store.dispatch({
    type: 'SELECT_SECTION',
    payload: 'video'
});
...
store.dispatch({
    type: 'SET_SECTION',
    payload: {
        key: 'stat',
        value: {
            a: 1
        }
    }
});
...
store.dispatch({
    type: 'SELECT_SECTION',
    payload: 'news'
});
...
store.dispatch({
    type: 'SET_SECTION',
    payload: {
        key: 'main',
        value: {
            content: 'text'
        }
    }
});

API

Base functionality (listate, dist/listate.js)

baseWhen(state, prevState): boolean

Checks whether current value (state) is not equal previous value (state).

Returns value of the following comparison: state !== prevState.

listen(store, listener): Function

Adds/registers state change listener for the given store.

Arguments:

  • store: object - Store for which listener should be added/registered.

  • listener: Function | object - Specifies listener that should be called on a state change. Can be a function or an object that defines listener settings/details.

  • listener.handle: Function - Listener that should be called on a state change.

  • listener.context: boolean | object (optional) - Specifies object that should be used as this value when calling the listener.

  • listener.data: any (optional) - Any data that should be passed into the listener.

  • listener.delay: number (optional) - Specifies that listener should be called after the given number of milliseconds have elapsed. Works similar to debounce: when several requests for the listener call arrive during the specified period only the last one will be applied after the timeout. 0 means that the listener should be called asynchronuosly.

  • listener.filter: (state) => state.part (optional) - Function (selector) to extract state part which will be used inside when to determine whether the listener should be called. By default the entire state will be used.

  • listener.once: boolean (optional) - Whether the listener should be called just once (by default false).

  • listener.when: (current, prev, data) => boolean (optional) - Function to determine whether the listener should be called. By default baseWhen is used. The listener will be called if the function returns true. The following parameters will be passed into the function:

    • The current state or a part of the current state if filter is set.
    • The previous state or a part of the previous state if filter is set.
    • An object that will be passed into listener.

Returns a function that removes/unsubscribes the listener.

An object with the following fileds will be passed as parameter into the listener:

  • current: any - The current state or a part of the current state if filter is set.
  • prev: any - The previous state or a part of the previous state if filter is set.
  • state: object - The current state.
  • prevState: object - The previous state.
  • data: any - The auxiliary data (value of listener.data parameter).
  • store: object - The store for which listener is registered.
  • dispatch: Function - Reference to dispatch method of the store.
  • unlisten: Function - The function that removes/unsubscribes the listener.

Extra functionality (listate/extra, dist/extra.js)

getPathValue(obj, path): any

Return value of specified field path inside given object.

import { getPathValue } from 'listate/extra';
const obj = {
    a: {
        b: {
           c: 'value'
        },
        d: true
    },
    e: 4,
    f: [1, 'z', null]
};
getPathValue(obj, 'a.b.c');   // 'value'
getPathValue(obj, 'a.c');   // undefined

getObjectPart(source, parts): object

Create an object containing specified parts of the given object.

import { getObjectPart } from 'listate/extra';
const obj = {
    a: {
        b: {
           c: 'value',
           d: true
        },
        e: 4,
        f: [1, 'z', null]
    },
    g: 7,
    h: {
        i: false,
        j: 0
    },
    k: 'king',
    l: 'last'
};
getObjectPart(obj, {f1: 'a.b.d', f2: 'a.f.1', f3: 'g', f4: 'h.j'});   // {f1: true, f2: 'z', f3: 7, f4: 0}

getFieldFilter(path): Function

Return a function that extracts value of the specified field path inside a given object.

import { getFieldFilter } from 'listate/extra';
const filter = getFieldFilter('a.d');
const obj = {
    a: {
        b: {
           c: 'value'
        },
        d: 17
    },
    e: 4,
    f: [1, 'z', null]
};
filter(obj);   // 17

getPartFilter(parts): Function

Return a function that creates an object containing the specified parts of a given object.

import { getPartFilter } from 'listate/extra';
const filter = getPartFilter({f1: 'a.b.c', f2: 'h.j', f3: 'k'});
const obj = {
    a: {
        b: {
           c: 'value',
           d: true
        },
        e: 4,
        f: [1, 'z', null]
    },
    g: 7,
    h: {
        i: false,
        j: 0
    },
    k: 'king',
    l: 'last'
};
filter(obj);   // {f1: 'value', f2: 0, f3: 'king'}

unlike(state, prevState, deep): boolean

Check whether current object (state) is not equal previous object (state) comparing values of their fields.

import { unlike } from 'listate/extra';
unlike({a: 1, b: 2}, {a: 1, b: 2});   // false
unlike({a: 1, b: {c: 3}}, {a: 1, b: {c: 3}});   // true
unlike({a: 1, b: {c: 3}}, {a: 1, b: {c: 3}}, true);   // false

unlikeDeep(state, prevState): boolean

Check whether current object (state) is not equal previous object (state) deeply comparing values of their fields.

The same as unlike(state, prevState, true).

listen(store, listener): Function

Add/register state change listener for the given store.

It is a wrap around base listate.listen that supports the following enhanced listener settings:

  • listener.filter. When an array or an object is passed, the used filter will be result of getPartFilter(listener.filter). When a string is passed, the used filter will be result of getFieldFilter(listener.filter).
  • listener.when. By default unlike is used.

See doc folder for details.

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code.

License

Copyright (c) 2017-2019 Denis Sikuler
Licensed under the MIT license.