Skip to content

Instantly share code, notes, and snippets.

@stockwellb
Created July 22, 2017 01:53
Show Gist options
  • Save stockwellb/0b18b8bd5aa3b69a5ae6b5860a0f59a8 to your computer and use it in GitHub Desktop.
Save stockwellb/0b18b8bd5aa3b69a5ae6b5860a0f59a8 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<title>TheImmutableFabFour</title>
<meta name="description" content="The Immutable Fab Four: Strategies for immutable JavaScript" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
</head>
<body>
<article id="main">
<time datetime="2017-06-19" pubdate>2017-06-19</time>
<h1>The Immutable Fab Four</h1>
<p>Strategies for immutable JavaScript</p>
<p>Immutable data is a concept that I had to deal with while writing my first reducer for ngrx/store. The idea behind immutable data is that it cannot be changed after creation. This simple shift in state management can make applications more simple to build and easier to debug. With a single source of truth you have a contract in hand that will free you of a whole class of problems that plague mutable applications.
I’ll share what I’ve learned so far. Keep in mind that these examples stem from my experiences using ngrx/store but are in no way directly tied it.</p>
<p><strong>Let’s start with some state:</strong> This state will consist of an object with an array of ids and an entities object that will store one key/value for each id in the ids array.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">let</span> states = [];
<span class="hljs-keyword">const</span> initialState = {
<span class="hljs-attr">ids</span>: [],
<span class="hljs-attr">entities</span>: {}
}
states = [...states, initialState]
</code></pre>
<p><strong>Adding some data to our empty state:</strong></p>
<pre><code class="language-javascript"><span class="hljs-comment">// add john</span>
<span class="hljs-keyword">const</span> john = {<span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'John Lennon'</span>};
<span class="hljs-keyword">const</span> solo = {
<span class="hljs-attr">ids</span>: [...initialState.ids, john.id],
<span class="hljs-attr">entities</span>: <span class="hljs-built_in">Object</span>.assign({}, initialState.entities, {[john.id]: john})
}
states = [...states, solo];
</code></pre>
<p>There’s a lot going on here. We’ll look at one thing at a time. <code>john</code> is the new object that we want to add to the state.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">const</span> john = {<span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'John Lennon'</span>};
</code></pre>
<p>From the object <code>john</code> we’ll extract the id property to add to the <code>ids</code> array and copy the id and name properties into the <code>entities</code> object.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">const</span> solo = {
<span class="hljs-attr">ids</span>: [] <span class="hljs-comment">//add new ids,</span>
entities: {} <span class="hljs-comment">//add new entities</span>
}
</code></pre>
<p><strong>Adding new ids:</strong> The spread operator is used to expand any existing ids in place, followed by the new object’s id. The <code>solo.ids</code> array will have no references to the <code>initialState.ids</code>.</p>
<pre><code class="language-javascript">ids: [...initialState.ids, john.id]
</code></pre>
<p><strong>Adding new entities:</strong> The <code>Object.assign</code> method is used to create a new entities object. The first argument is a newly created object followed <code>initialState.entities</code>. The key/values from <code>initialState.entities</code> will be shallow copied into the new empty object. Lastly, the object <code>{[john.id]: john}</code> will be merged with the newly created object.</p>
<pre><code class="language-javascript">entities: <span class="hljs-built_in">Object</span>.assign({}, initialState.entities, {[john.id]: john})
</code></pre>
<p>In the end the state solo will look like this.</p>
<pre><code class="language-javascript">{
<span class="hljs-attr">ids</span>: [<span class="hljs-number">1</span>],
<span class="hljs-attr">entities</span>: {
<span class="hljs-number">1</span>: {<span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'John Lennon'</span>}
}
}
</code></pre>
<p><strong>Adding more data:</strong> The <code>others</code> array contains new objects to be added to the state. We’ll need to extract the ids from array and reduce the objects in it to one single object.</p>
<pre><code class="language-javascript"><span class="hljs-comment">// add paul, george and ringo</span>
<span class="hljs-keyword">const</span> others = [
{<span class="hljs-attr">id</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'Paul McCartney'</span>},
{<span class="hljs-attr">id</span>: <span class="hljs-number">3</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'George Harrison'</span>},
{<span class="hljs-attr">id</span>: <span class="hljs-number">4</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'Ringo Starr'</span>}
];
<span class="hljs-keyword">let</span> newIds = others.map(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x.id);
<span class="hljs-keyword">let</span> newEntities = others.reduce(<span class="hljs-function">(<span class="hljs-params">acc, member</span>) =&gt;</span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Object</span>.assign(acc, {[member.id]: member});
},{})
<span class="hljs-keyword">const</span> fabFour = {
<span class="hljs-attr">ids</span>: [...solo.ids, ...newIds],
<span class="hljs-attr">entities</span>: <span class="hljs-built_in">Object</span>.assign({}, solo.entities, newEntities)
};
states = [...states, fabFour];
</code></pre>
<p><strong>First the ids:</strong> <code>newIds = others.map(x =&gt; x.id)</code> creates a new array named <code>newIds</code> that has no reference to the <code>others</code> array. Next combine <code>newIds</code> with <code>solo.ids</code> by using the spread operator like this. <code>ids: […solo.ids, …others.map(x =&gt; x.id)]</code>.</p>
<p><strong>The entities will take a little more work:</strong> <code>reduce</code> and <code>Object.assign</code> are used together to shallow copy all the members of the others array into a single new object called <code>newEntities</code>. As before, there are no references to the old data. The new data has been copied. No mutations have occurred.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">let</span> newEntities = others.reduce(<span class="hljs-function">(<span class="hljs-params">acc, member</span>) =&gt;</span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Object</span>.assign(acc, {[member.id]: member});
},{})
</code></pre>
<p><strong>Assembling the state:</strong> It’s as simple as copying the existing data and the new data into a new object. The spread operator is used to expand the existing <code>solo.ids</code> and the <code>newIds</code> , which are combined together into the <code>fabFour.ids</code> array. <code>Object.assign</code> is used to merge the existing key/value pairs of <code>solo.entities</code> and <code>newEntities</code> into the new empty object <code>{}</code>.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">const</span> fabFour = {
<span class="hljs-attr">ids</span>: [...solo.ids, ...newIds],
<span class="hljs-attr">entities</span>: <span class="hljs-built_in">Object</span>.assign({}, solo.entities, newEntities)
};
</code></pre>
<p><strong>Adding more data:</strong> With some data in the state, let’s take another look at adding a new object. This should look very familiar. This is the same process used to add <code>john</code> to the earlier state.</p>
<pre><code class="language-javascript"><span class="hljs-comment">//add billy</span>
<span class="hljs-keyword">const</span> billy = {<span class="hljs-attr">id</span>: <span class="hljs-number">5</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'Billy Preston'</span>};
<span class="hljs-keyword">const</span> fabFourV1 = {
<span class="hljs-attr">ids</span>: [...fabFour.ids, billy.id],
<span class="hljs-attr">entities</span>: <span class="hljs-built_in">Object</span>.assign({}, fabFour.entities, {[billy.id]: billy})
}
</code></pre>
<p><strong>Removing and replacing data:</strong> Let’s remove <code>billy</code> and replace it with <code>eric</code> . The strategies are the same as we’ve used so far. First, filter the ids to <code>newIds</code>. <code>newIds</code> will no longer contain <code>billy.id</code>. Next, reduce <code>newIds</code> down to the <code>newEntities</code> object looking up each item from the <code>newIds</code> array in the <code>fabFourV1.entities</code> object. In the end <code>newEntities</code> will no longer contain the <code>billy</code> object. Finally, merge <code>eric.id</code> with the filtered <code>newIds</code> and merge <code>eric</code> with the filtered <code>newEntities</code>.</p>
<pre><code class="language-javascript"><span class="hljs-comment">//remove billy and replace him with eric</span>
<span class="hljs-keyword">const</span> eric = {<span class="hljs-attr">id</span>: <span class="hljs-number">6</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">'Eric Clapton'</span>};
newIds = fabFourV1.ids.filter(<span class="hljs-function"><span class="hljs-params">x</span> =&gt;</span> x != billy.id)
newEntities = newIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> fabFourV1.entities[id]).reduce(<span class="hljs-function">(<span class="hljs-params">acc, member</span>) =&gt;</span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Object</span>.assign(acc, {[member.id]: member});
},{});
fabFourV2 = {
<span class="hljs-attr">ids</span>: [...newIds, eric.id],
<span class="hljs-attr">entities</span>: <span class="hljs-built_in">Object</span>.assign({}, newEntities, {[eric.id]: eric})
}
</code></pre>
<hr>
<p>There have been <strong>5 states</strong> created in this exercise; <code>initialState</code>, <code>solo</code>, <code>fabFour</code>, <code>fabFourV1</code>, <code>fabFourV2</code>. Each state was created with immutable methods. There are no references shared between them.</p>
<pre><code class="language-javascript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>, len = states.length; i &lt; len; i++) {
<span class="hljs-built_in">console</span>.log(states[i].ids.map(<span class="hljs-function"><span class="hljs-params">id</span> =&gt;</span> states[i].entities[id].name));
}
</code></pre>
<p>This output shows that each state is a separate object.</p>
<pre><code class="language-javascript">[]
[<span class="hljs-string">"John Lennon"</span>]
[<span class="hljs-string">"John Lennon"</span>, <span class="hljs-string">"Paul McCartney"</span>, <span class="hljs-string">"George Harrison"</span>, <span class="hljs-string">"Ringo Starr"</span>]
[<span class="hljs-string">"John Lennon"</span>, <span class="hljs-string">"Paul McCartney"</span>, <span class="hljs-string">"George Harrison"</span>, <span class="hljs-string">"Ringo Starr"</span>, <span class="hljs-string">"Billy Preston"</span>]
[<span class="hljs-string">"John Lennon"</span>, <span class="hljs-string">"Paul McCartney"</span>, <span class="hljs-string">"George Harrison"</span>, <span class="hljs-string">"Ringo Starr"</span>, <span class="hljs-string">"Eric Clapton"</span>]
</code></pre>
<p>No states were mutated during the creation of this article. The full source can be found <a href="https://gist.github.com/stockwellb/b27c29d655866d6abd03fd6c849b71df">here</a>.</p>
</article>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment