npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

sprout-data

v1.3.0

Published

A set of functions to work with nested data.

Downloads

433

Readme

Sprout

Build Status npm version tree-shakeable minzipped size semantic versioning

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.

Working with nested data

Retrieving nested values

Sprout's get() function allows you to gracefully retrieve nested values without blowing up when a key isn't present.

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

const z = sprout.get(data, ['x', 'y', 'z'], []);
z.filter(...);

Modifying nested values

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:

  • The data may get used in other places. Modifying it in-place can lead to unexpected results and makes it harder to reason about
  • You have no easy way of telling when it was changed
  • There's no easy way to roll back to an earlier version of your data

Luckily, Sprout also has a solution for this problem.

Immutability

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.

Structural sharing

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:

const data = {
  a: {b: {c: 1}},
  x: {y: {z: 1}}
};

const updatedData = sprout.assoc(data, ['a', 'b', 'c'], 2);
  • updatedData.a.b.c === 2
  • data is not mutated, therefore data.a.b.c === 1
  • The objects 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)

No unnecessary changes

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.

const data = {a: 1};

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

Installation

npm install sprout-data --save

or

yarn add sprout-data

or just download and include sprout.js or sprout.min.js in your page.

API

The path argument can be a single key or an array of keys to access nested properties.

get(obj, path, [defaultValue])

Get a (nested) property from an object. Returns undefined or – if provided – the defaultValue if any key in the path doesn't exist.

const { get } = require('sprout-data');
const 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'

assoc(obj, path, value, [path2, value2, ...])

Assigns a value to a path in obj. Multiple path-value pairs can be specified.

const { assoc } = require('sprout-data');
const 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'}}

dissoc(obj, path, [path2, ...])

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.

const { dissoc } = require('sprout-data');
const 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'}

update(obj, path, fn, [args])

Applies fn to a property at path from obj. Optional args will be supplied to fn.

const { update } = require('sprout-data');
const 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}}

merge(obj, obj2, [obj3, ...])

(Shallow) merge obj2's properties into obj. Properties of the rightmost object will override existing properties.

const { merge } = require('sprout-data');
const 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}

deepMerge(obj, obj2, [obj3, ...])

Deep-merge obj2's properties into obj. Properties of the rightmost object will override existing properties. Non-existing objects will be created.

const { deepMerge } = require('sprout-data');
const 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.

Performance

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.

Inspiration

Author

Jeremy Stucki, Interactive Things

License

BSD, see LICENSE