jquery-connect

Easily connect your jQuery code to stores like Redux

Usage no npm install needed!

<script type="module">
  import jqueryConnect from 'https://cdn.skypack.dev/jquery-connect';
</script>

README

jquery-connect logo

jquery-connect

Easily connect your jQuery code to stores like Redux.
Flexible and very small.

Installation

In the browser

The easiest way to use jquery-connect is to include it with a <script> tag:

<script src="https://unpkg.com/jquery-connect"></script>

This file is already compiled into ES5 and will work in any modern browsers.

Via npm

jquery-connect is also avalaible through npm:

yarn add jquery-connect

You can then import it in your code:

import 'jquery-connect';

As a side-effect library, jquery-connect will be executed as soon as it'll be included in your code. Please be sure that jQuery is included before it.

jquery-connect is a jQuery plugin: that means it requires jQuery to work.
It supports jQuery v1.7.0 and higher.

Usage

jquery-connect provides a connect method to jQuery objects:

$('#foo').connect(/* ... */);

With jquery-connect, you connect a jQuery object to a render function with a store:

$('#foo').connect(myRenderFunction, myStore);

About the rendering function

myRenderFunction is a simple function you define. It's called in the context of the corresponding DOM element, so the keyword this refers to the element:

function myRenderFunction() {
  $(this).text('I am rendered!');
}

myRenderFunction will be fired at init and every time jquery-connect detects a change in the connected store.

About the store

The store is an object handling a part of your application state. The common way to use stores is via Redux:

const myStore = Redux.createStore(/* ... */);

Even if this plugin has been built with Redux in mind, it is not required. It will work as soon as the store you provide exposes these three methods:

  • subscribe(renderFunction)
  • getState()
  • dispatch(action)

What does "render" mean in jquery-connect?

The rendering function you provide to connect() method will be fired every time a render is required. Basically, "render" means "run the rendering function".

A render is required at init and every time jquery-connect detects a change in the connected store.

Connect an element to some parts of the store

If your application is big, chances are it's the same for your stores.
First, remember you can use multiple stores, no needs to have only one. Please only remember that elements can be connected to only one store.

Anyway, an element connected to a store will be rendered every time a change occurs in its connected store. If the rendering function uses only some values from the store and not all of them, it's a waste of resources to call it again.

Here comes to play the mapper function. connect() method indeed accepts as third parameter a function. This function takes as argument the whole state of the connected store, and its returned value will be send to the rendering function:

// myRenderFunction will only receive { foo, bar } as argument instead of the whole state
$('#foo').connect(myRenderFunction, myStore, function (state) {
  return {
    foo: state.foo,
    bar: state.bar,
  };
});

By doing that, the elements are connected only to the provided parts of the state. In the above example, it means that myRenderFunction will be called only if properties foo and bar are updated. If another value from the state is update, the element won't be re-rendered.
Please check the corresponding example to know more about this feature.

Dispatch actions from rendering function

Instead of calling directly store.dispatch() from the rendering function, it's better to use the mapper function to inject dispatch:

// The mapper function receives dispatch function as second argument
$('#foo').connect(myRenderFunction, myStore, function (state, dispatch) {
  return {
    foo: state.foo,
    onSomeEvent: (bar) => dispatch({ type: 'whatever', payload: bar });
  };
});

function myRenderFunction({ foo, onSomeEvent }) {
  /* ... */
  // Calling onSomeEvent('fraise'); will then dispatch action { type: 'whatever', payload: 'fraise' }
  /* ... */
}

By doing like this, you don't tie your rendering function to a specific store, meaning it'll be easier to reuse it if needed.

Warnings with the rendering function

It's very important to understand that the rendering function will be triggered as it is on each render. It means that if we listen for some events in the rendering function, these events will be listened one more time on each render.
Basically, if we do that:

// This is NOT ok!
function myRenderFunction(value) {
  $(this).click(function() { alert(value); });
}

Each time the connected element will be rendered, a click listener will be attached: the alert will therefore be triggered too many times! This example on CodePen illustrates the issue.

The easiest solution to fix the problem is just to remove the click listener before attaching a new one (like the second button in the example from the above link):

// This is ok
function myRenderFunction(value) {
  $(this).off('click').click(function() { alert(value); });
}

But sometimes, it could be trickier: maybe we want to define a window.setInterval or perform an API call. In that case, we may need to use sideEffect.

Calling connect() with sideEffect

The connect() method can be called with 'sideEffect' as first argument. In that case, its second argument must be a function:

function renderingFunction() {
  $(this).connect('sideEffect', function() { /* ... */ });
}

This function will called in the same context than the rendering function, meaning that this still refers to the DOM element. But above all, this function will be run only once, during the first render.

function renderingFunction() {
  $(this).connect('sideEffect', function() { alert('You will see me only once!'); });
}

When calling connect() with sideEffect, it also possible to send a third argument: dependencies. It could be any primitive values or an array of values.
When providing dependencies, the sideEffect callback won't be called only once, but at each render if and only if the dependencies have changed:

function renderingFunction({ foo, bar }) {
  $(this).connect('sideEffect', function() {
    console.log('Foo has changed! New value:', foo);
  }, foo);
}

If the sideEffect callback returns a function, this function will be automatically called before next render:

function renderingFunction({ foo, bar }) {
  $(this).connect('sideEffect', function() {
    console.log('Foo has changed! New value:', foo);
    
    return function() {
      console.log('Foo will change! Old value:', foo);
    };
  }, foo);
}

See full example on CodePen

Please also note you can call sideEffect only from a rendering function. It will throw if you try to call it outside.

Examples

Hello World

The simplest (and useless) example:

$(function() {
  // A function returning a static name
  function getName() {
    return 'World'; // Try updating this value!
  }
  
  // Create a Redux store from the above function
  const helloStore = Redux.createStore(getName);
  
  // Define a rendering function
  function renderHello(name) {
    $(this).text('Hello ' + name + '!');
  }
  
  // Connect a jQuery object to our rendering function with our store
  $('#hello').connect(renderHello, helloStore);
});

Edit on CodePen

Connected form

A more concrete example with an actual reducer and dispatched actions:

$(function() {
  // The reducer handling the state
  function reducer(state = 'World', action) {
    if (action.type === 'set') {
      return action.payload;
    }
    
    return state;
  }
  
  // Create a Redux store from the above reducer
  const helloStore = Redux.createStore(reducer);
  
  // Define a rendering function
  function renderHello(name) {
    $(this).text('Hello ' + name + '!');
  }
  
  // Connect a jQuery object to our rendering function with our store
  $('#hello').connect(renderHello, helloStore);
  
  // Init field value with store state
  $('#field').val(helloStore.getState());
  
  // On form submit, dispatch field value to set the new state
  $('#form').submit(function(e) {
    e.preventDefault();
    helloStore.dispatch({
      type: 'set',
      payload: $('#field').val(),
    });
  });
});

Edit on CodePen

Connect multiple elements to same store

This example is the same than the previous one, except that it shows we can connect a store to multiple elements to make ou life easier:

$(function() {
  /* ... */
  
  // Connect the paragraph
  $('#hello').connect(renderHello, helloStore);
  
  // Connect the field to the same store
  $('#field').connect(renderField, helloStore);
  
  /* ... */
  
  // When clicking on "fraise" button, dispatch "Fraise" to set the state
  $('#fraise').click(function() {
    helloStore.dispatch({
      type: 'set',
      payload: 'Fraise',
    });
    
    // No needs to manually set the value of the field here!
  });
});

See full code on CodePen

Connect elements to some parts of the store

This example shows how to provide a function as third parameter to connect elements to some parts of the store state:

$(function() {
  /* ... */
  
  // This figure is only connected to the "color" property of the store
  $('#figure-color').connect(renderFigureColor, store, function (state) {
    return state.color;
  });
  
  // This figure is only connected to the "shape" property of the store
  $('#figure-shape').connect(renderFigureShape, store, function (state) {
    return state.shape;
  });
  
  /* ... */
});

See full code on CodePen

Using sideEffect

This example shows how sideEffect works:

$(function() {
  /* ... */
  
  function renderField(value) {
    $(this)
      .val(value)
      .connect('sideEffect', function() {
        console.log('Value has changed! New value:', value);
    
        return function() {
          console.log('Value will change! Old value:', value);
        };
      }, value);
  }
  
  /* ... */
});

See full code on CodePen

Calling an API

Calling an external API from the rendering function should be done with sideEffect:

$(function() {
  /* ... */
  
  function renderGames({ search, onRequestEnd }) {
    const $this = $(this);
    
    // Use sideEffect to be sure we try to call the API only when "search" is updated
    $this.connect('sideEffect', function() {
      if (!search) {
        $this.html('<li>Please search for a game</li>');
      } else {
        getGames(search).then(function (games) {
          // Call function received from props and send total of games
          onRequestEnd(games.length);
          
          if (!games.length) {
            $this.html('<li>No results found</li>');
            return;
          }

          $this.html(games.map(function (game) {
            return (
              `<li class="game">
                <img src="${game.image_url}" alt="" />
                <b>${game.name}</b>
              </li>`
            );
          }));
        }); 
      }
      
      // The returned function of sideEffect will be called before next render.
      // In our case, it'll be just before performing another search.
      // Try slower your Internet speed to see the loading after submiting the form!
      return function () {
        $this.html('<li>Loading...</li>');
      };
    }, search);
  };
  
  /* ... */
});

See full code on CodePen