Cursor Immutable Data Helper for Reactjs

Cursor Immutable Data Helper for Reactjs

Utility designed for immutable data in a React flux architecture.

PUI Cursor

Utility designed for immutable data in a React flux architecture.

Cursors

PUI Cursors are simplified versions of Om Cursors designed for use with a React Flux architecture. It enables targeted, immutable updates to data; these updates are particularly useful for updating a store in React.

A cursor takes in data and a callback. The callback is used to propagate data into an app and create a new cursor with the updated data.

A minimal example of cursor setup is below:

const Cursor = require('pui-cursor');
const React = require('react');
const Zoo = require('./zoo');

class Application extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state.store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
  }

  render() {
    const $store = new Cursor(this.state.store, updatedStore => this.setState({store: updatedStore}));

    return <Zoo animals={this.state.store.animals} $store={$store}/>;
  }
}

Our convention is to prefix Cursor instances with $, like $store in the above example. This convention differentiates the cursor from the data it contains.

For example in this setup, if the Zoo component calls this.props.$store.merge({visitors: ['Charles', 'Adam', 'Elena']});, the application store will now have visitors in addition to animals.

Timing

When the cursor is updated, the callback is called asynchronously (inside of a setImmediate() under the hood). This is to handle multiple synchronous updates to the cursor. The updates are batched together into a single callback.

Synchronous Mode

If you want to use synchronous callbacks, you can enable synchronous mode by setting

Cursor.async = false;

In synchronous mode, synchronous updates to the cursor are no longer batched. This can lead to many more callbacks and a reduction in performance. We recommend using synchronous mode only for unit tests.

Common Asynchronous Mistakes

Accessing the store before it updates

Using asynchronous callbacks can lead to unexpected behavior when accessing the store.

For example:

var store = [1,2];
const $store = new Cursor(store, callback);

If you update the cursor and try to access the store synchronously,

$store.push(3);
console.log($store.get());

you might expect the console to print [1,2,3]. Instead the console will print [1,2] because the callback has not fired yet.

You can use the React lifecycle methods such as componentWillReceiveProps or componentDidUpdate to work around this. For example, if you add the following function to a component that has the store as a prop,

componentWillReceiveProps(nextProps) {
  if (nextProps.store !== this.props.store) {
    console.log(nextProps.store);
  }
}

the console will print [1,2,3].

Stale Cursors

Another, more subtle, problem might arise from storing the cursor as a variable. If you are in a component with $store on props, you might want to write code like the following:

var $store = this.props.$store;
doSomethingAsync().then(function(something) {
  $store.push(something);
});

This code will work in isolation, but it has a race condition. If some other code updates the cursor (i.e. $store.push("otherThing")) while you are waiting for doSomethingAsync to resolve, the active cursor has updated to include "otherThing". When doSomethingAsync resolves, the handler attached to it will update the old cursor (that does not include "otherThing"). The callback will be called with the old store, which does not have "otherThing".

This bug can be hard to diagnose, so cursors will print a "You are updating a stale cursor" warning in the console when a stale cursor is being updated.

The safer version of the code is:

doSomethingAsync().then((function(something){
  this.props.$store.push(something);
}).bind(this));

This ensures that the component uses the most recent version of the store when updating.

API

PUI Cursor provides wrappers for the React immutability helpers. These wrappers allow you to transform the data in your cursor; the transformation you specify is applied and the new result is used to update the cursor value.

get()

Returns your current node

var store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
const $store = new Cursor(store, callback);

The cursor never updates its own data structure, so get is prone to returning stale data.

If you execute $store.refine('animals', 'lion').set('Scar').get();, it will return 'Larry' instead of 'Scar'

In general, we recommend that you not use get and instead access the store directly with props. If you want to use get, ensure that you are using the newest version of your Cursor.

set()

Sets the data for your current node. If you call `set at the top of the data tree, it sets the data for every node.

var store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
const $store = new Cursor(store, callback);

If you execute $store.refine('animals').set({lion: 'Simba', warthog: 'Pumba'});, the callback will be called with {animals: {lion: 'Simba', warthog: 'Pumba'}}.

refine()

Changes where you are in the data tree. You can provide refine with multiple arguments to take you deeper into the tree.

If the data node that you're on is an object, refine expects a string that corresponds to a key in the object.

var store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
const $store = new Cursor(store, callback);

For example, $store.refine('animals', 'seal').get();, will return 'Sebastian'.

If the data node that you're on is an array of objects, refine expects an index or an element of the array.

var hey = {greeting: 'hey'};
var hi = {greeting: 'hi'};
var hello = {greeting: 'hello'};
var store = {greetings: [hey, hi, hello]};
const $store = new Cursor(store, callback);

then $store.refine('greetings', 1, 'greeting').get(); will return 'hi'. If you have the element of an array but not the index, $store.refine('greetings', hi, 'greeting').get(); will also return 'hi'.

merge()

Merges data onto the object at your current node

$store.refine('animals').merge({squirrel: 'Stumpy'});

The callback will be called with {animals: {lion: 'Larry', seal: 'Sebastian', squirrel: 'Stumpy'}}.

push()

Pushes to the array at your current node

var hey = {greeting: 'hey'};
var hi = {greeting: 'hi'};
var hello = {greeting: 'hello'};
var yo = {grettings: 'yo'};
var store = {greetings: [hey, hi, hello]};
const $store = new Cursor(store, callback);

If you execute $store.refine('greetings').push({greeting: 'yo'});, the callback will be called with {greetings: [hey, hi, hello, yo]}.

apply()

If the simpler functions like set, merge, or push cannot describe the update you need, you can always call apply to specify an arbitrary transformation.

Example:

var currentData = {foo: 'bar'};
var cursor = new Cursor(currentData, function(newData){ this.setState({data: newData}); });
cursor.apply(function(shallowCloneOfOldData) {
  shallowCloneOfOldData.foo += 'bar';
  return shallowCloneOfOldData;
});

Warning: The callback for apply is given a shallow clone of your data (this is the behavior of the apply function in the React immutability helpers). This can cause unintended side effects, illustrated in the following example:

var currentData = {animals: {mammals: {felines: 'tiger'}}};
var cursor = new Cursor(currentData, function(newData){ this.setState({data: newData}); });

cursor.apply(function(shallowCloneOfOldData) {
  shallowCloneOfOldData.animals.mammals.felines = 'lion';
  return shallowCloneOfOldData;
});

Since the data passed into the callback is a shallow clone of the old data, values that are nested more than one level deep are not copied, so shallowCloneOfOldData.animals.mammals will refer to the exact same object in memory as currentData.animals.mammals.

The above version of apply will mutate the previous data in the cursor (currentData) in addition to updating the cursor. As a side effect, shallow compare will not detect any changes in the data when it compares previous props and new props. To safely use apply on nested data, you need to use the React immutability helpers directly:

var reactUpdate = require('react/lib/update');

cursor.apply(function(shallowCloneOfOldData) {
  return reactUpdate.apply(shallowCloneOfOldData, {
    animals: {
      mammals: {
        felines: {$set: 'lion'}
      }
    }
  });
});

remove()

Removes your current node

If the current node is an object and you call remove(key), remove deletes the key-value.

var store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
const $store = new Cursor(store, callback);

If you execute $store.refine('animals', 'seal').remove();, the callback will be called with {animals: {lion: 'Larry'}}.

If the current node is an array:

var hey = {greeting: 'hey'};
var hi = {greeting: 'hi'};
var hello = {greeting: 'hello'};
var store = {greetings: [hey, hi, hello]};
const $store = new Cursor(store, callback);

If you execute $store.refine('greetings').remove(hello), the callback will be called with {greetings: [hey, hi]}.

splice()

Splices an array in a very similar way to array.splice. It expects an array of 3 elements as an argument. The first element is the starting index, the second is how many elements from the start you want to replace, and the third is what you will replace those elements with.

var hey = {greeting: 'hey'};
var hi = {greeting: 'hi'};
var hello = {greeting: 'hello'};
var yo = {greeting: 'yo'};
var store = {greetings: [hey, hi, hello]};
const $store = new Cursor(store, callback);

If you execute $store.refine('greetings').splice([2, 1, yo]);, the callback will be called with {greetings: [hey, hi, yo]}.

unshift()

Adds an element to the start of the array at the current node.

var hey = {greeting: 'hey'};
var hi = {greeting: 'hi'};
var hello = {greeting: 'hello'};
var yo = {greeting: 'yo'};
var store = {greetings: [hey, hi, hello]};
const $store = new Cursor(store, callback);

If you execute $store.refine('greetings').unshift(yo);, the callback will be called with {greetings: [yo, hey, hi, hello]}

Download Details:

Author: pivotal-cf

Source Code: https://github.com/pivotal-cf/pui-cursor

reactjs react javascript

What is Geek Coin

What is GeekCash, Geek Token

Best Visual Studio Code Themes of 2021

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

How native is React Native? | React Native vs Native App Development

Article covers: How native is react native?, React Native vs (Ionic, Cordova), Similarities and difference between React Native and Native App Development.

What is ReactJS? | ReactJS Basics | Learn ReactJS | React for Beginners | ReactJS Training

This Edureka video on "What is ReactJS ?" will help you understand the fundamentals of ReactJS and help you in building a strong foundation in React by understanding the advantages of ReactJS along with its features and major aspects.

Increase Performance of React Applications Via Array JavaScript Methods

Increase Performance of React Applications Via Array JavaScript Methods. We will create a simple event management application in the react to add, update, and delete an event.

Routing in React without React-Router

I have been using React JS in my projects for quite some time now and am used to managing routing in my app using the react-router package. I have always been keen on having as little dependencies in my apps as possible, so, I always felt perturbed by the use of this particular package in simpler apps which did not have complex routes.

The Ugly Side of React Hooks

In this post, I will share my own point of view about React Hooks, and as the title of this post implies, I am not a big fan.