Skip to content
This repository was archived by the owner on Sep 3, 2022. It is now read-only.

Commit 2abfc76

Browse files
committed
Keep component state immutable when using setState
1. Updated how setState works when using a callback. 2. The prevState is now a copy of the original component state. 3. This means that callback must return prevState for component state to be updated. 4. This, of course, will also trigger a re-render of the component, if state did change. 5. Refactored component update method to throttle updates at intervals of 16.66 milliseconds (60 frames per second). 6. Updated component.html test for setState changes.
1 parent 72e0247 commit 2abfc76

File tree

9 files changed

+72
-55
lines changed

9 files changed

+72
-55
lines changed

dist/composi.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/composi.js.gzip

38 Bytes
Binary file not shown.

dist/composi.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/component.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isObject, isSameNode, mixin } from './utils'
1+
import { isObject, EMPTY_OBJECT, EMPTY_ARRAY, isSameNode, mixin } from './utils'
22
import { patch } from './vdom'
33

44
/**
@@ -216,7 +216,7 @@ export class Component {
216216
*/
217217
set state(data) {
218218
this[dataStore] = data
219-
setTimeout(() => this.update())
219+
setTimeout(() => this.update(), 1000 / 60)
220220
}
221221

222222
/**
@@ -232,11 +232,19 @@ export class Component {
232232
*/
233233
setState(data) {
234234
if (typeof data === 'function') {
235-
const state = data.call(this, this.state)
236-
if (state) this.state = state
235+
let copyOfState
236+
if (isObject(this.state)) {
237+
copyOfState = mixin(EMPTY_OBJECT, this.state)
238+
} else if (Array.isArray(this.state)) {
239+
copyOfState = EMPTY_ARRAY.concat(EMPTY_ARRAY, this.state)
240+
} else {
241+
copyOfState = this.state
242+
}
243+
const newState = data.call(this, copyOfState)
244+
if (newState) this.state = newState
237245
} else if (isObject(this.state) && isObject(data)) {
238-
const state = this.state
239-
this.state = mixin(state, data)
246+
const newState = mixin(this.state, data)
247+
this.state = newState
240248
} else {
241249
this.state = data
242250
}

lib/utils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export const SVG_NS = 'http://www.w3.org/2000/svg'
1616
*/
1717
export const EMPTY_OBJECT = {}
1818

19+
/**
20+
* An empty array. Used for access to array methods.
21+
* @type {any[]} EMPTY_ARRAY
22+
*/
23+
export const EMPTY_ARRAY = []
24+
1925
/**
2026
* Combine two objects, merging the second into the first. Any properties already existing in the first will be replaced by those of the second. Any properties in the second not in the first will be added to it.
2127
* @param {Object.<string, any>} firstObject

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "composi",
3-
"version": "2.6.4",
3+
"version": "2.6.5",
44
"description": "A JavaScript library for creating websites, PWAs and hybrid apps.",
55
"main": "index.js",
66
"scripts": {

resources/dev/css/styles.css

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
/* Bootstrap 4 Reset */
22

3+
/*!
4+
* Bootstrap Reboot v4.1.3 (https://getbootstrap.com/)
5+
* Copyright 2011-2018 The Bootstrap Authors
6+
* Copyright 2011-2018 Twitter, Inc.
7+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8+
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
9+
*/
310
*,
411
*::before,
512
*::after {
@@ -12,31 +19,30 @@ html {
1219
-webkit-text-size-adjust: 100%;
1320
-ms-text-size-adjust: 100%;
1421
-ms-overflow-style: scrollbar;
15-
-webkit-tap-highlight-color: transparent;
16-
font-size: 62.5%;
22+
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
1723
}
1824

1925
@-ms-viewport {
2026
width: device-width;
2127
}
2228

23-
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
29+
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
2430
display: block;
2531
}
2632

2733
body {
2834
margin: 0;
29-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
30-
font-size: 1.4rem;
31-
font-weight: normal;
35+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
36+
font-size: 1rem;
37+
font-weight: 400;
3238
line-height: 1.5;
3339
color: #212529;
3440
text-align: left;
3541
background-color: #fff;
3642
}
3743

3844
[tabindex="-1"]:focus {
39-
outline: none !important;
45+
outline: 0 !important;
4046
}
4147

4248
hr {
@@ -47,7 +53,7 @@ hr {
4753

4854
h1, h2, h3, h4, h5, h6 {
4955
margin-top: 0;
50-
margin-bottom: .5rem;
56+
margin-bottom: 0.5rem;
5157
}
5258

5359
p {
@@ -59,7 +65,7 @@ abbr[title],
5965
abbr[data-original-title] {
6066
text-decoration: underline;
6167
-webkit-text-decoration: underline dotted;
62-
text-decoration: underline dotted;
68+
text-decoration: underline dotted;
6369
cursor: help;
6470
border-bottom: 0;
6571
}
@@ -85,7 +91,7 @@ ul ol {
8591
}
8692

8793
dt {
88-
font-weight: bold;
94+
font-weight: 700;
8995
}
9096

9197
dd {
@@ -143,7 +149,7 @@ a:not([href]):not([tabindex]) {
143149
text-decoration: none;
144150
}
145151

146-
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
152+
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
147153
color: inherit;
148154
text-decoration: none;
149155
}
@@ -156,7 +162,7 @@ pre,
156162
code,
157163
kbd,
158164
samp {
159-
font-family: monospace, monospace;
165+
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
160166
font-size: 1em;
161167
}
162168

@@ -176,21 +182,9 @@ img {
176182
border-style: none;
177183
}
178184

179-
svg:not(:root) {
185+
svg {
180186
overflow: hidden;
181-
}
182-
183-
a,
184-
area,
185-
button,
186-
[role="button"],
187-
input:not([type=range]),
188-
label,
189-
select,
190-
summary,
191-
textarea {
192-
-ms-touch-action: manipulation;
193-
touch-action: manipulation;
187+
vertical-align: middle;
194188
}
195189

196190
table {
@@ -200,7 +194,7 @@ table {
200194
caption {
201195
padding-top: 0.75rem;
202196
padding-bottom: 0.75rem;
203-
color: #868e96;
197+
color: #6c757d;
204198
text-align: left;
205199
caption-side: bottom;
206200
}
@@ -211,14 +205,11 @@ th {
211205

212206
label {
213207
display: inline-block;
214-
margin-bottom: .5rem;
208+
margin-bottom: 0.5rem;
215209
}
216210

217211
button {
218212
border-radius: 0;
219-
border: solid 1px #ccc;
220-
background-color: #ccc;
221-
overflow: hidden;
222213
}
223214

224215
button:focus {
@@ -274,6 +265,10 @@ input[type="datetime-local"],
274265
input[type="month"] {
275266
-webkit-appearance: listbox;
276267
}
268+
input[type='text'],
269+
input[type='number'] {
270+
border: solid 1px #ccc;
271+
}
277272

278273
textarea {
279274
overflow: auto;
@@ -329,6 +324,7 @@ output {
329324

330325
summary {
331326
display: list-item;
327+
cursor: pointer;
332328
}
333329

334330
template {
@@ -339,16 +335,6 @@ template {
339335
display: none !important;
340336
}
341337

342-
html, body, h1, h2, h3, h4, h5, h6, p, ol, ul, li, dl,
343-
dt, dd, blockquote, address, table {
344-
margin: 0;
345-
padding: 0;
346-
}
347-
348-
* {
349-
font-weight: 100;
350-
}
351-
352338

353339
/****************/
354340
/* Local Styles */

test/component.html

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ <h1>Mocha Tests - Composi Component Class</h1>
3535
<script>
3636

3737
const {h, Component} = composi
38-
let should = chai.should()
39-
let expect = chai.expect
38+
const should = chai.should()
39+
const expect = chai.expect
4040

4141
describe('Create Component Instance', function() {
4242
let componentDidMount = false;
@@ -302,7 +302,7 @@ <h1>Mocha Tests - Composi Component Class</h1>
302302
})
303303

304304

305-
it('On update, list should have four children', function(done) {
305+
it('After setting state, list should have four children', function(done) {
306306
setTimeout(function() {
307307
list2.setState(prevState => {
308308
prevState.push('Onions')
@@ -312,13 +312,30 @@ <h1>Mocha Tests - Composi Component Class</h1>
312312
}, 300)
313313
done()
314314
})
315-
it('On update, last list item should contain "Onions"', function(done) {
315+
it('After setting state, last list item should contain "Onions"', function(done) {
316316
setTimeout(function() {
317317
expect(listEl.children[4].textContent.trim()).to.equal('Onions')
318318
}, 400)
319319
done()
320320
})
321321

322+
it('When setting state, original state should remain immutable.', function(done) {
323+
setTimeout(function() {
324+
// prevState should be a copy of the component's state.
325+
// That means changes to it should not affect the component's state.
326+
// Here we do not return prevState, preventing it from updating original state with changes.
327+
let newState = []
328+
list2.setState(prevState => {
329+
prevState.push('new stuff')
330+
newState = prevState
331+
})
332+
// Since we never returned prevState above, the component's original state should remain unchanged:
333+
expect(JSON.stringify(list2.state)).to.equal(JSON.stringify(['Potatoes', 'Tomatoes', 'Carrots', 'Peas', 'Onions']))
334+
// But the changes to prevState should be as follows:
335+
expect(JSON.stringify(newState)).to.equal(JSON.stringify(['Potatoes', 'Tomatoes', 'Carrots', 'Peas', 'Onions', 'new stuff']))
336+
}, 600)
337+
done()
338+
})
322339

323340

324341
it('Component lifecycle methods "componentWillUpdate" and "componentDidUpdate" should run when component state is updated', function(done) {

0 commit comments

Comments
 (0)