Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
sprout-data
Advanced tools
Sprout provides a set of functions to help you work with nested data without all the headaches. Sprout never mutates the original data but returns new versions. This way, plain JavaScript objects (and arrays) can be effectively treated as if they were immutable.
One useful application of this would be to modify application state using Sprout and store each version in an array to get instant undo/redo functionality. Or you could only re-render changed subtrees of the application state by comparing with strict equality between versions.
Sprout's get()
function allows you to gracefully retrieve nested values without blowing up when a key isn't present.
var data = {a: {b: {c: 'foo'}}};
Normally, you'd retrieve the value of c
like this:
data.a.b.c; // => 'foo'
But what if the data isn't structured like you expected? Let's say
data = {a: {b: {}}};
data.a.b.c; // => undefined
That still looks good, right? But what if your data looks like
data = {};
data.a.b.c; // => Uh-oh! "TypeError: Cannot read property 'b' of undefined"
You could prevent this by checking for the existence of nested objects first:
data.a && data.a.b ? data.a.b.c : void 0;
But who wants to write code like that?
Sprout to the rescue!
sprout.get(data, ['a', 'b', 'c']) // => undefined
Additionally, you can supply a default return value as the third parameter to sprout.get()
. This is useful for example when you expect an array and want to call its methods later.
var z = sprout.get(data, ['x', 'y', 'z'], []);
z.filter(...);
When you want to modify your data, you'll usually do it this way:
data = {a: {b: {c: 'foo'}}};
data.a.b.c = 'bar';
This works fine in this particular case but has the same problem when your data isn't shaped like you expect.
data = {};
data.a.b.c = 'bar'; // => Again: "TypeError: Cannot read property 'b' of undefined"
Whereas with Sprout you can do this without worrying about the existence of nested objects:
sprout.assoc(data, ['a', 'b', 'c'], 'bar') // => {a: {b: {c: 'bar'}}}
Also, the naive approach of data.a.b.c = x
mutates your original data. Although it is the most performant way of modifying data, it's bad for several reasons:
Luckily, Sprout also has a solution for this problem.
Sprout never mutates your data. Whenever a change is applied, a new version is returned.
The easy approach to achieve this would be to create a deep copy of your data and then modify the copy. But especially for larger data structures, this is performance- and memory-intensive since you'll always copy everything instead of just the necessary parts.
For efficiency, Sprout uses structural sharing. This means it re-uses unmodified parts of your data. This is more performant and memory-efficient than deep-copying. It also lets you compare individual parts with strict equality to detect what has changed. Consider the following scenario:
var data, updatedData;
data = {
a: {b: {c: 1}},
x: {y: {z: 1}}
};
updatedData = sprout.assoc(data, ['a', 'b', 'c'], 2);
updatedData.a.b.c === 2
data
is not mutated, therefore data.a.b.c === 1
updatedData.x
and updatedData.x.y
are re-used from data
, therefore updatedData.x === data.x
and updatedData.x.y === data.x.y
(and of course updatedData.x.y.z === data.x.y.z
)When an operation doesn't actually change a value (i.e. when the new value is strictly equal to the old one), Sprout doesn't create new objects at all and returns the original unmodified data instead.
data = {a: 1};
updatedData = sprout.assoc(data, 'a', 1);
In this case, updatedData === data
.
The data itself is not made immutable (by calling Object.freeze
or wrapping it). Therefore it's still possible to mutate the original data using other methods if you're not careful.
npm install sprout-data --save
or
bower install sprout-data --save
or just download and include sprout.js
or sprout.min.js
in your page.
The path argument can be a single key or an array of keys to access nested properties.
Get a (nested) property from an object. Returns undefined
or – if provided – the defaultValue if any key in the path doesn't exist.
var get = require('sprout-data').get;
var obj = {a: 'foo', b: {c: 'bar'}};
// Get a property
get(obj, 'a') // => 'foo'
// Get a nested property
get(obj, ['b', 'c']) // => 'bar'
// Getting an non-existing property
get(obj, ['b', 'd']) // => undefined
// Gracefully handles non-existing keys (as opposed to obj.x.y which would throw an error because it can't access y of obj.x)
get(obj, ['x', 'y']) // => undefined
// Define a default return value for non-existing properties
get(obj, ['b', 'd'], 'not found') // => 'not found'
Assigns a value to a path in obj. Multiple path-value pairs can be specified.
var assoc = require('sprout-data').assoc;
var obj = {a: 'foo', b: {c: 'bar'}};
// Change a property
assoc(obj, 'a', 'baz'); // => {a: 'baz', b: {c: 'bar'}}
// Change a nested property
assoc(obj, ['b', 'c'], 'baz'); // => {a: 'foo', b: {c: 'baz'}}
// New objects are created when they don't exist already
assoc(obj, ['b', 'd', 'e'], 'baz'); // => {a: 'foo', b: {c: 'bar', d: {e: 'baz'}}}
// Change multiple nested properties at once
assoc(obj, ['b', 'c'], 'baz', ['b', 'd'], 'blah'}}); // => {a: 'foo', b: {c: 'baz', d: 'blah'}}
Removes a property at path from obj. Multiple paths can be specified to remove multiple properties. If all properties are removed from an object, the object itself will also be removed.
var dissoc = require('sprout-data').dissoc;
var obj = {a: 'foo', b: {c: 'bar', d: 1, e: 'baz'}};
// Remove a property
dissoc(obj, 'a'); // => {b: {c: 'bar', d: 1, e: 'baz'}}
// Remove a nested property (empty objects are removed)
dissoc(obj, ['b', 'c']); // => {a: 'foo', b: {d: 1, e: 'baz'}}
// Remove multiple nested properties at once (where keys match)
dissoc(obj, ['b', 'c'], ['b', 'd']); // => {a: 'foo', b: {e: 'baz'}}
// Removing all properties of an object also removes the object from its parent
dissoc(obj, ['b', 'c'], ['b', 'd'], ['b', 'e']); // => {a: 'foo'}
Applies fn to a property at path from obj. Optional args will be supplied to fn.
var update = require('sprout-data').update;
var obj = {a: 1, b: {c: 2}};
// Update a property
update(obj, 'a', function(v) { return v + 1; }); // => {a: 2, b: {c: 2}}
// Update a nested property
update(obj, ['b', 'c'], function(v) { return v + 1; }); // => {a: 1, b: {c: 3}}
// Supply additional arguments to fn
function add(x, y) { return x + y; }
update(obj, ['b', 'c'], add, 5); // => {a: 1, b: {c: 7}}
(Shallow) merge obj2's properties into obj. Properties of the rightmost object will override existing properties.
var merge = require('sprout-data').merge;
var obj = {a: 1, b: {c: 2}};
// Merge
merge(obj, {d: 5}); // => {a: 1, b: {c: 2}, d: 5}
// Merge multiple objects
merge(obj, {d: 5, e: 6}, {d: 10}); // => {a: 1, b: {c: 2}, d: 10, e: 6}
Deep-merge obj2's properties into obj. Properties of the rightmost object will override existing properties. Non-existing objects will be created.
var deepMerge = require('sprout-data').deepMerge;
var obj = {a: 1, b: {c: 2, d: 3}};
// Deep merge
deepMerge(obj, {b: {d: 5}}); // => {a: 1, b: {c: 2, d: 5}}
// Non-existing objects along the path will be created
deepMerge(obj, {x: {y: 42}}); // => {a: 1, b: {c: 2, d: 3}, x: {y: 42}}
See tests for more details.
For what it's worth, I benchmarked Sprout against mori, immutability-helper and two deep clone algorithms.
These are the results for node v10.15.3 on MacBook Pro 2016:
mori native x 3,262,639 ops/sec ±0.51% (89 runs sampled)
sprout.assoc x 1,206,109 ops/sec ±1.12% (84 runs sampled)
clone x 482,803 ops/sec ±0.46% (88 runs sampled)
mori to js x 387,704 ops/sec ±0.85% (89 runs sampled)
Lodash clone x 314,781 ops/sec ±0.70% (86 runs sampled)
immutability-helper x 279,670 ops/sec ±0.89% (90 runs sampled)
mori total conversion x 101,457 ops/sec ±0.74% (86 runs sampled)
What's noteworthy is that mori is by far the fastest, as long as no conversion happens between mori and JavaScript data structures. So if you're using mori all the way in your app, it's probably the best solution, as you also get real immutable data structures.
Jeremy Stucki, Interactive Things
BSD, see LICENSE
FAQs
A set of functions to work with nested data.
The npm package sprout-data receives a total of 31 weekly downloads. As such, sprout-data popularity was classified as not popular.
We found that sprout-data demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.