pivotal-cf/pui-cursor

Name: pui-cursor

Owner: Pivotal Cloud Foundry

Description: Cursor immutable data helper for Reactjs

Created: 2015-02-28 00:35:35.0

Updated: 2017-12-18 19:49:39.0

Pushed: 2018-02-16 18:02:11.0

Homepage: null

Size: 124

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

PUI Cursor

npm version Build Status Dependencies

Utility designed for immutable data in a React flux architecture.

Table of Contents
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:

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

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


nder() {
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

or.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:

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

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

re.push(3);
ole.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,

onentWillReceiveProps(nextProps) {
 (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:

$store = this.props.$store;
methingAsync().then(function(something) {
tore.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:

methingAsync().then((function(something){
is.props.$store.push(something);
ind(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

store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
t $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.

store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
t $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.

store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
t $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.

hey = {greeting: 'hey'};
hi = {greeting: 'hi'};
hello = {greeting: 'hello'};
store = {greetings: [hey, hi, hello]};
t $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

re.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

hey = {greeting: 'hey'};
hi = {greeting: 'hi'};
hello = {greeting: 'hello'};
yo = {grettings: 'yo'};
store = {greetings: [hey, hi, hello]};
t $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:

currentData = {foo: 'bar'};
cursor = new Cursor(currentData, function(newData){ this.setState({data: newData}); });
or.apply(function(shallowCloneOfOldData) {
allowCloneOfOldData.foo += 'bar';
turn 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:

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

or.apply(function(shallowCloneOfOldData) {
allowCloneOfOldData.animals.mammals.felines = 'lion';
turn 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:

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

or.apply(function(shallowCloneOfOldData) {
turn 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.

store = {animals: {lion: 'Larry', seal: 'Sebastian'}};
t $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:

hey = {greeting: 'hey'};
hi = {greeting: 'hi'};
hello = {greeting: 'hello'};
store = {greetings: [hey, hi, hello]};
t $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.

hey = {greeting: 'hey'};
hi = {greeting: 'hi'};
hello = {greeting: 'hello'};
yo = {greeting: 'yo'};
store = {greetings: [hey, hi, hello]};
t $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.

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

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


(c) Copyright 2016 Pivotal Software, Inc. All Rights Reserved.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.