DH2642 Interaction programming VT23
Cristian Bogdan
Course team dh2642-ta@eecs.kth.se
Cristian Bogdan, course leader, examiner
Edward Leander, amanuens, head TA
9 TAs, they have all gone through the lab tutorial before
Course Objectives
DH2642 Intended Learning Outcomes: Having passed the course, the student will be able to
See also grading criteria
Hederskodex
https://www.kth.se/eecs/utbildning/hederskodex/inledning-1.17237
Measure Of Software Similarity- MOSS
See also lab coding conventions. They emphasize code originality in function names and parameter names!
Members of a lab or project group are expected to commit their work to git individually.
Please join a TW2_TW3 group ASAP!
For any problem (repository, git-ssh, TW1…) please file an issue
https://gits-15.sys.kth.se/iprog/issues
If you can’t file an issue, check that you have an account at https://gits-15.sys.kth.se. If not, make one
Only if you can’t file an issue or don’t have a repo, dh2642-ta@eecs.kth.se
Please book 5 min redovisning times, individual (details later)
Examination
Labs 3 hp (P/F + advanced points)
Project 4,5hp A-F (becomes course grade)
Lab Test result database
All the lab test results are recorded
So we can help you personally, or detect issues that seem hard for most
You can opt out, or be anonymous
We perform learning analytics (anonymized data)
You can ask later for your data to be removed (GDPR)
OLI Question-based learning
The material is not complete but includes JS basics and IxP basics
At the bottom left of the Canvas page, click OLI Torus SE�https://canvas.kth.se/courses/37574/external_tools/3761
Course development
160 of 180 students finished in HT20 (compared to 60 of 100 the year before). The tutorial lab is certainly a good idea. 227 of 243 in HT21, ~200 of 213 HT22
New tutorial for VT23 (v 5.0). About 25 slides per tutorial week (as before). Tutorial goal: prepare for the project.
VT23: tutorial v5.2
We work with two frameworks (React and Vue) but in TW3 you can choose one of them. JSX (UI) code is the same for both frameworks.
Previously students also found it difficult to navigate the course material.
Course Schedule
The minimum JavaScript you need in DH2642
JavaScript basics: Object
Object literals Objects are Dictionaries: mapping from keys to values
const car= { doors:2, make:"Ferrari", "model":"Testarossa" , };
// comma after last property allowed
car.doors // → 2
car["doors"] // -> 2
car.doors=3 // property assignment, same as car["doors"]=3
const x= "make"; // let for variables, rarely used
car[x] // -> "Ferrari", reading from Dictionary (get)
car[x]= "Porsche" // writing in Dictionary (put)
car.someProp // → undefined, i.e. somebody forgot something. Use null to indicate “empty”
car.someProp.wheels //TypeError cannot read properties of undefined
car.someProp?.wheels // → undefined optional chaining operator ?.
car.inexistentMethod() //TypeError inexistentMethod is not a function
car.inexitentMethod?.() // → undefined
neverUse = 27; // global scope, implicit declaration, NEVER use!!!
Note the data types (primitives):
Integer: 3 number: 3.14
string "my 'string' here" or 'my \'string\' here'
boolean: true, false
Always use this in JS classes to refer object fields (aka properties)
class CleverClass{
constructor(param){
this.someProperty= param; // right
console.log(someProperty) ; // ReferenceError: someProperty is not defined
console.log(this.someProperty) ; // right
otherProperty= param; // wrong, creates an implicit global otherProperty!!!
this.otherProperty= param; // right
} // same goes for methods!
method(param){
this.aProperty= param +1;
}
}
JavaScript basics: Array
Array literals
[6, 5, 1]
const cars= [ { doors:2, make: "Ferrari", model: "Testarossa", } ,
{ doors:4, make: "Porsche", model: "Cayenne", } ,
{ doors:2, make: "Ferrari", model: "Dino" }, // comma after last element allowed
];
cars[0] // → the Testarossa array[index] rarely used, Array.filter, Array.map
cars.length // → 3, array.length rarely used, see above
cars["0"] // → the Testarossa. Arrays are actually objects (see Dictionary get)
Array spread
const cars2= [...cars, oneMoreCar ]; // appends to cars array without changing it (immutable)
// or just a clone: [...cars]
Operators and automatic operand type conversion
=== !== do not convert (recommended!!!) 1!=="1" // → true 1==="1" false!
== != attempt conversion 1!= "1" // → false! 1=="1" is true
For many other operations it’s harder to escape conversion!
1 + true // → 2
1 + "2" //→ "12"
1 + Number.parseInt("2") // → 3
Truthy and falsy (automatic conversion to true and false)
if(42) console.log("this will surely print!"); // 42 is truthy
if(0) console.log("this will never print!"); // 0 is falsy. So are "", false, null, undefined…
42 || whatever //→ 42 (whatever is never evaluated).
0 && whatever //→ 0 (whatever is never evaluated).
Note that 42 and 0 are returned, not true and false! (C evaluation)
if(), not always the best option with UI (JSX)
const test=42;�if(test){ � console.log("will print because 42 is truthy"); �}
const falsyTest=""; // or undefined, null, 0
if(falsyTest) � // a single statement, curly braces optional� console.log("won't print");
else{� console.log("will print because the condition was falsy");�}
Problem: in JSX the user interface is a large expression. If we want to show some UI conditionally, if() which is a statement is hard to use.
Solution: condition?someUI:someOtherUI or condition && someUI
throw and try/catch, often not in the same function. Stacktraces are your friend!
function canGoWrong(str){
if(str.length===0) // same as !str
throw new Error("empty strings not allowed");
return str.toUpperCase();
}
function printString(s){
try{
console.log(canGoWrong(s));
}catch(e){ console.error(e); }
finally{ console.log("this will be printed anyway"); }
}
printString("")
Destructuring (a simple syntactic convenience)
Goal: create multiple constant/variables at once, from a given array (or object)
function makeArray(){ return ["one", "two", "ignored"];}
Alternative 1 (traditional) Without array destructuring. Takes 3 Lines of Code:
const arr= makeArray(); // we only want to call the function once!
const first= arr[0];
const second= arr[1];
Alternative 2 (JS) With array destructuring (used mostly with React state, but you can skip it as above)
const [first, second]= makeArray(); // one-liner. We skip using indexes :)
Object destructuring (you can easily live without it, but you may see it in tutorials, stackoverflow, …)
const myObect={ prop1: "value1", prop2: "value2"};
const {prop1, prop2}= myObject; // instead of const prop1=myObject.prop1 etc
Destructuring in function parameters (not used in lecture materials, but you may see it used in tutorials…)
function arrCleverFunction([first, second], anotherParam) { console.log(first, second); }
function objCleverFunction(somParam, {prop1, prop2}) { console.log(prop1, prop2); }
== or === between truthy objects are rarely useful
This is true for Java and other programming languages. But it is good to recap.
obj1== obj2 basically checks if obj1 and obj2 are the same object. The same location in memory. The same “box”. Comparison by reference, not by value.
Better test some object property (e.g. id) instead:
obj1.id == obj2.id
That will indicate that we have two copies of the “same” object (the same dish for example)
Avoiding the most frequent JavaScript errors with ?.
const obj={ prop1:42, prop2: { someProp: "value"} } ;
obj.prop2.someProp // → 'value'
obj.prop3 //→ undefined
obj.prop3.someProp // TypeError: cannot read properties of undefined
obj.prop3?.someProp // → undefined
obj.inexistentMethod()// TypeError: inexistentMethod is not a function
obj.inexistentMethod?.() // → undefined
Callbacks
The single most important concept in Interaction Programming
Arrays have methods. Intro to callbacks
[6,1,5].sort() // -> [1,5,6] elements are sorted as strings! "9">"84"!
But what if we want to sort descending?
function compareNumbersCB(a,b){ // a callback!� if(a<b) { return 1; } � if(a>b) return -1; // no need for {} if only one statement� return 0; // no need for else after return �} // sort(CB) only looks for positive/negative/zero, therefore: return b-a;
[6,1,5].sort(compareNumbersCB) // pass the callback! -> [6,5,1]�
What if we want to sort cars?�function compareCarsCB(car1, car2){ return car2.doors - car1.doors; }
[...cars].sort(compareCarsCB) // [Cayenne, Testarossa, Dino]
Think of a callback as a plugin for an algorithm.
Programming 101
My Code
Library
Interaction programming (among others)
My Code (callbacks)
Framework
Library (toolkit)
Inversion of control
Hollywood principle
Template method (e.g. sort)
Callback checklist Array.sort(CB)
How to pass the callback? array.sort(CB) Other params needed besides the callback? No
What does array.sort(CB) return? Same array! Sorted. Important! Sort a copy to leave array unchanged! [...arr].sort(CB)
When does array.sort(CB) return? When array is sorted // this may seem like a stupid question, but there are no stupid questions
Who calls the callback, and sends parameters? sort() execution
Role (and usual name?) of callback parameters elem1, elem2 for comparison
What is the callback expected to return? <0 → keep order, >0 → reverse, 0 equal
When is the callback called? When sort needs a comparison, according to the sorting algorithm it uses
Is the callback called once? Or repeatedly? Zero or more times
What is the role of the callback? Comparator
Example callback names compareCarsCB
Callback checklist Array.filter(CB)
function hasAtLeastFourDoorsCB(car) { return car.doors > 3; }
How to pass the callback? array.filter(CB) Other params needed besides the callback? No
What does Array.filter(CB) return? New array, with only some elements kept
When does array.filter(CB) return? After all array elements are visited
Who calls the callback, and sends parameters? filter()
Role (and usual name?) of callback parameters element optional: index, array
What is the callback expected to return? truthy→ keep in returned array, falsy→ don’t
When is the callback called? Once per element, to decide whether to keep in result
Is the callback called once? Or repeatedly? Once per element
What is the role of the callback? Tester
Example callback names testCarCB, hasAtLeastFourDoorsCB
Callback checklist Array.map(CB)
function carToTextCB(car) { return car.make+" "+car.model; }�function carToDoorsCB(car) { return car.doors; }
How to pass the callback? array.map(CB) Other params needed besides the callback? No
What does Array.map(CB) return? New array, with all elements transformed by the CB
When does array.map(CB) return? After all array elements are visited
Who calls the callback, and sends parameters? map()
Role (and usual name?) of callback parameters element optional: index, array
What is the callback expected to return? Element in resulting array
When is the callback called? Once per element
Is the callback called once? Or repeatedly? Once per element
What is the role of the callback? Transformer
Example callback names car2TextCB , car2DoorsCB
Callback checklist Array.reduce(CB, initialAcc) less important in the course
function sumCarDoorsReducerCB(accumulator, car) { return accumulator + car.doors; }
cars.reduce(sumCarReducerCB, 0)
How to pass the callback? array.reduce(CB, initialAccumulator) Other params needed besides the callback? yes
What does Array.reduce(CB, initialAcc) return? Final value of accumulator, after applying CB to all elements
When does array.filter(CB) return? After all array elements are visited
Who calls the callback, and sends parameters? reduce()
Role (and usual name?) of callback parameters accumulator, element optional: index, array
What is the callback expected to return? New accumulator
When is the callback called? Once per element
Is the callback called once? Or repeatedly? Once per element
What is the role of the callback? Reducer
Example callback names sumReducerCB , sumCarDoorsReducerCB
function sumReducerCB(accumulator, element) { return accumulator + element; } // general!
map-reduce
function sumReducerCB(accumulator, element) { return accumulator + element; } // general!
Just reduce cars.reduce(sumCarDoorsReducerCB, 0)
map-reduce cars.map(car2doorsCB).reduce(sumReducerCB, 0)
Array.reduce is very powerful / general, but in the course we only use reduce for sums. If you only do sums, with map-reduce you only need a single reducer like sumReducerCB
Just for fun: filter and map can be written with reduce:
function myMap(array, transformer){
function transformReducerCB(acc, elem){ return [...acc, transformer(elem)]; }
return array.reduce(transformReducerCB, []); // an array as accumulator! �}
function myFilter(array, tester){
function keepReducerCB(acc, elem){
if(tester(elem)) return [...acc, elem];
return acc; // no need for else after return
} // alternative: return tester(elem)?[...acc, elem]:acc
return array.reduce(keepReducerCB, []); // an array as accumulator!
}
Lab coding conventions (Obligatory)
Why?
const someCB= anonymousFunction meets the objectives (1) and maybe (3) but not 2. Therefore it is not acceptable. Internally the function is still anonymous, even if it is assigned to a const!
Callback checklist setTimeout(ACB, millis) / setInterval
How to pass the callback? setTimeout(ACB, millis) Other params needed besides the callback? Optional, default zero
What does setTimeout(ACB, millis) return? An integer timeout id, for clearTimeout(id). Value 3 above
When does setTimeout(ACB, millis) return? Immediately (just makes a plan that when x millis pass, ACB will be invoked)
Who calls the callback, and sends parameters? browser timer / node.js timer
Role (and usual name?) of callback parameters none
What is the callback expected to return? nothing
When is the callback called? When millis has passed
Is the callback called once? Or repeatedly? Once (see setInterval for repeated regularly)
What is the role of the callback? Do something later / without disturbing the current execution flow
JS functions run to completion. setTimeout(ACB, 0) will execute ACB some time *after* the current function completes!
Example callback names doLaterACB
A= asyncrhonous
Callback roles in Interaction Programming
functional Components (TW1; called back at first render and on update) no need for ACB, name starts with capital letter
Event listeners (called back when user interacts, or UI initialized / torn down, TW1)
Custom events (called back when a custom Component wants to notify its parent, TW1)
Array rendering (Array.map(callback), TW1). Synchronous (CB) unlike most others in this list, which are asynchronous (ACB)
Promises (callback invoked when a response has arrived from a Web API, TW2)
Component lifecycle (callback invoked when component created, updated, destroyed, TW3)
Subscribe to object changes (Observer TW3, Proxy, RxJS, Redux store.subscribe(ACB))
Redux reducer (bonus). Often synchronous. Redux as “application state” (not UI!) framework
Sometimes we receive a callback e.g. from the Framework, or from our own code
Basic HTML, JSX
Basic HTML http://localhost:8080/html-lecture.html
<html>
<head>
<title>DH2642 test</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="debug">
<button>click me</button>
<input placeholder="type here"/>
<select size="1">
<option>My way</option>
<option>The highway</option>
</select>
</div>
</body>
</html>
/* style.css */
.debug{ border: 1px solid red;}
/* selector { property: value; prop:val; ..}*/
Basic HTML Elements
HTML Element attributes
Graphical user interfaces represented internally as trees
DIV class� BUTTON� Click me!
INPUT placeholder
SELECT size
OPTION
My way
OPTION
The highway
Indentation in this tree representation depicts parent-child relationship. We will use this representation in the lab!
UIs are represented as trees in most GUI technologies.
ELEMENTS can have children. Children of the same parent are called siblings
Static text are called Nodes. Cannot have children (tree leaf)
The first child of BUTTON
is the node “Click me”
The next sibling of BUTTON
is the element INPUT
In the browser Developer Tools you can see the current tree under Elements. For static HTML it is the same as the HTML code found in Sources
Procedural UI construction using the DOM�Not used in the lab!
The DOM (Document Object Model) is not used directly in the course but it is the Toolkit that Frameworks use!
HTML is a declarative way of describing UI. You can also compose UI procedurally using the DOM toolkit, for example test this at the Console, on an empty page!
const input= document.createElement("input");
input.setAttribute("placeholder", "type here");
const btn= document.createElement("button");
btn.append("click me");
document.body.append(btn); // different order than above!
document.body.append(input);
// and even procedural update
document.querySelector("button").firstChild.textContent= "Do not click!";
Frameworks achieve Declarative update using State and component re-render.
Troubles with HTML
We could address these by generating the UI procedurally, in JS directly! JS has variables, loops, subprograms…
Can we address these but keep the Declarative aspects?
Solutions:
Basic JSX (server-less version, Babel runs in browser)
<html>
<head>
<script src="https://unpkg.com/@babel/standalone@7.12.12/babel.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const React={createElement:Vue.h}
</script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="root"></div>
</body>
<script type="text/jsx" src="myApp.js"></script>
</html>
// myApp.js:
const holder="please type...";
const ui=<div class="debug">
<button>click me</button>
<input placeholder={holder}/>
<select>
<option>My way</option>
<option>The highway</option>
</select>
</div>;
Vue.render(ui, document.getElementById("root"));
Babel translates from JSX to JS. It adds dynamically some scripts in the <head>!
Vue is the framework we use to render the JSX (we could have used React as well)
Modern apps usually render a single DIV, in which the framework renders the UI
We do not use the serverless set-up (any longer) but it’s good to know that it exists. You can see it at work in the course demo app
https://www.csc.kth.se/~cristi/pokemon-explorer
Basic JSX (webpack version, Babel run by Webpack at server)
HTML template, webpack adds one or more <script > to it.
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
In the lab we use static styling (CSS), served by webpack from public/style.css . Webpack has more advanced CSS functionality, not used in the lab. You can use it in the project.
Webpack makes one big file out of all JS used (our JS/JSX, the framework, all the dependencies) and injects it into the HTML <head>.
New approach: Vite
// test-react.js
import React from "react";// needed for JSX, don’t use in the lab!
import {render} from "react-dom";
const holder="please type:";
const ui= <div class="debug">
<button>click me</button>
<input placeholder={holder}/>
<select>
<option>My way</option>
<option>The highway</option>
</select>
</div>;
render(ui, document.getElementById("root"));
// test-vue.js
import {h, render} from "vue";
const React= {createElement:h}; // inject Vue.h to handle JSX
// the lab does the Vue/React choice automatically
// the rest is the same! React and Vue JSX are almost identical
Custom component (ACB!)
The MyComponent function is called a functional custom component �Name must begin with a capital letter which is unusual in JavaScript/C/Java coding conventions.
The framework can then render� <MyComponent holder="please type" prop2={value2} />
All props are sent as a single object parameter to the MyComponent function
// folder/myComp.js�export default function someNameACB(props){ ..}�// names don’t have to be the same but usually are
// folder/otherFile.js�import MyComponent from "./myComp.js"
// .. later ..�<MyComponent prop1="someConst" prop2={value2} />
// somewhereElse/someFile.js�import MyComponent from "/folder/myComp.js"
// or import MyComponent from "../folder/myComp.js"
function MyComponent(props){
return ( // return alone on a line returns undefined!
<div class="debug">� <button>click me</button>� <input placeholder={props.holder}/>� <select>� <option>My way</option>� <option>The highway</option>� </select> � </div>� );�}
function MyComponent2(props){� return <div>� … same as above� </div> ;�}
render(<div><MyComponent placeholder="please type"/></div>,
document.getElementById("root"));
Custom components and Props
Code that uses the component:
<CustomComponent prop1={expr1} prop2={expr2} />
Component definition:
function CustomComponent(props){ // props will now be the object { prop1:expr1, prop2:expr2 }
// code uses props.prop1 , props.prop2
// here props is an ordinary JS object!
// like any JS func parameter, props can have any name.
// function CustomComponent(pucko){ /* use pucko.prop1, pucko.prop2 */ }
}
“Pass foo as a prop” means that we add � <CustomComponent foo={y} prop1={expr1} prop2={expr2} />
Then in the Custom component code we can use props.foo
Native (“born in the browser”, not custom) element: <button disabled={true}>Can’t click</button>
Like any JS object, props can contain “anything”
A string constant: query="constant" props.query
A string: query={someString+"suffix"} props.query
A number: nr={x*y/42} props.nr
A boolean: isDishInMenu={n>0} props.isDishInMenu
An array dishes={[d1, d2]} props.dishes.map(someCB)
Another object model={exprThatReturnsObj} props.model.numberOfGuests
A function! onNumberChange={someACB} props.onNumberChange(param)
model={props.model} means that a component receives a prop called model and it passes it on to its child (“prop drilling”)
When passing a function, the prop is called callback prop, or custom event. Details later!
TW1.2 Basic rendering
DIV� BUTTON disabled� -� number� BUTTON� +
DOM tree
number
SidebarView
Learning goal
File name
Props (bold)
disabled
<button />
View we work on
Native element
Attribute
UI test file, links:
Indentation: parent→child relationships in the DOM tree.
Static text
Italics: variable text (based on props)
Graphical appearance (sketch)
src/views/sidebarView.js
Link to this slide guide
TW1.2 Basic rendering
DIV
├── BUTTON disabled
│ └── “-”
├── number
└── BUTTON
└── “+”
number
SidebarView
Props (bold)
disabled
<button />
View we work on
Native element
Attribute
Static text
Italics: variable text (based on props)
Graphical appearance (sketch)
src/views/sidebarView.js
Link to this slide guide
JSX and the produced tree. Spaces matter!
const opt="My way";
<option>{ opt }</option>
Produces a single child:
OPTION
My way
<option> { opt }</option>
Produces two children:
OPTION
space
The highway
The framework or browser may or may not concatenate them to one child. Either way, the spaces will end up in the user interface:
OPTION
The highway
The extra space may affect the Unit tests. Ensure that there are no spaces between the tag and the curly braces! <tag>{ .. }</tag>
Spaces inside { } do not matter, as all spaces in JavaScript expressions!
Multiple consecutive spaces/tabs in DOM nodes are usually represented as one single space in the UI (thus hard to spot!) The following will look the same (although the DOM is different!).
<span>bla<!-- 4 spaces --> bla</span>
<span>bla<!-- 1 space --> bla</span>
Exception: HTML element <pre> some text </pre> (preformatted)
Check the Developer Tools Elements tab!
Callback checklist JSX functional component: createElement(ACB,props)
How to pass the callback? <MyComponent prop1={value1} prop2= {value2} /> Other params needed besides the callback? No
<MyComponent {... someObject } />
<MyComponent .. > <Child1.. /> <Child2 .. /></MyComponent>� React.createElement(MyComponent, props, children) � Vue.h(MyComponent, props, children)
What does <MyComponent /> return? Framework-dependent. Some plan to call MyComponent(props)
When does <MyComponent /> return? Immediately. (just makes a plan that when rendering/update is needed ACB will be invoked)
Who calls the callback, and sends parameters? The framework.
Role (and usual name?) of callback parameters One parameter, a JS object. It is usually called props but it’s just a convention
What is the callback expected to return? JSX, user interface
When is the callback called? At initial render, and every time the framework deems the component must update
Is the callback called once? Or repeatedly? Repeatedly
What is the role of the callback? Render user interface in a reusable manner
Example callback names Depends on the role of the component in the overall UI / project
What color is your curly brace? { { { {
Code block function doSomething(parms){ if(cond){console.log(x);} else {..} return x; }
Object creation literal const car= { doors:2, make:"Ferrari", model:"Testarossa" , };
JS expression in JSX <div> The title is: {props.title} !! Really? </div>
<SomeComponent someProp={props.title} someOtherProp={ someJSExpr } />
<SomeComponent {... car } />
<SomeComponent doors={car.doors} make={car.make} model={..} />
<div> The title is: {/* a JS comment */ props.title} !! Really? </div>
Object destructuring const {doors, make}= car;
function doSth({doors, make}){..} // useful to destructure props
Array rendering, key
Rendering repetitive UI (lists of items) is a frequent use case in UI rendering.
Special features in e.g. Angular (ng-for) or Vue templates (v-for).
No special features needed in JSX. Use Array.map
function dish2JSX_CB(dish){ return <li>{dish.name}</li>; } // LI = List Item
function SomeComponent(props){ // UL = Unordered List
return <ul>{ props.dishes.map(dish2JSX_CB) }</ul> ;
}
The framework will ask for a unique key per item, to help it update the UI more efficiently.
function dish2JSX_CB(dish){ return <li key={dish.id} >{dish.name}</li>; }
[{name:”Pasta”}, {name:”Pizza”}, {name:”Dolce”}] Arrray.map()
[<LI>Pasta</LI>, <LI>Pizza</LI>, <LI>Dolce</LI>] <ul>
Nesting functions
You often you have to nest functions because you will need parameters from the outer function
function SomeComponent(props){
// must be nested because it uses an outer function parameter (props)
function dish2JSX_CB(dish){
return <li key={dish.id} >{props.prefix} {dish.name} </li> ;
}
return <ul>{ props.dishes.map(dish2JSX_CB) }</ul> ;
}
// can also define the nested function after the return statement! function hoisting
When debugging, the current variables / parameters of both functions will be visible: dish2JSX_CB (Local) and SomeComponent (Closure)
TW1.3 Array rendering, basic CSS
DIV� BUTTON� -� numberOfGuests� BUTTON� +� TABLE� TBODY� TR key (array rendering)� TD� BUTTON� x� TD� A href
dishName test/dishesConst.js to figure which dish properties to use� TD� dishType import necessary function from utilities!� TD class same right-align CSS class as in SummaryView� dishPrice multiply with the number prop! Exactly two decimals. � TR� TD (empty)� TD class� Total:� TD (empty)� TD � totalPrice import necessary function! multiply with number! Two decimals. �
SidebarView
src/views/sidebarView.js
number
dishes
Prop received | Type | Comments/hints |
dishes | array | In the TBODY there will be a TR for each dish. Sort the dishes ! |
Attribute sent | Type | Comments/hints |
href | string constant | The hyperlink destination. Must be "#" for single-page applications |
Conditional rendering
if(condition) return <ComplexComponent prop={value1} />
/*else */ return <ComplexComponent prop={value2} />
We prefer expressions instead of statements. �We simply use JavaScript boolean expressions (truthy / falsy )
<ComplexComponent prop={condition ? value1 : value2} />
condition && <ComplexComponent prop={value} /> � only evaluates and renders ComplexComponent if condition is true
condition && <ComplexComponent1 prop={value} /> || <ComplexComponent2/> renders
Similar to condition ? <ComplexComponent1 prop={value} /> : <ComplexComponent2/>
Styling with CSS. Basics
Typographic styling
Stylesheets
Are typically kept in separate files (.css)
div {
width: 10px;
padding: 20px;
border: 1px solid lightblue;
background-color: green;
}
#main {
background-color: orange;
}
.red { /* CSS class */
background-color: red;
}
.blue {
background-color: blue;
}
#id beats
.className beats
element (e.g. DIV).
It’s basically a precedence rule
<head>
<title>My CSS experiment</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div></div>
<div id="main"></div>
<div class="red"></div>
<div class="blue red"></div>
<div id="main" class="red blue"></div>
</body>
In stylesheets you can combine selectors like div#main, div.red etc. See also attribute selectors and pseudo selectors
See Codepen
Typographic HTML/CSS: display inline, block, float
DIVs are like paragraphs (take whole width available to them). This is driven by the style property display, value block default for DIVs and P, H1, etc�display:block is default style for DIV
SPANs are like words (take only width necessary, and flow to the next line if there’s no space) �display:inline is default style for SPANs
display:inline-block is like inline but allows setting of height and width (unlike display:inline). So we can get “taller words” in the “text line”. Example: �display:inline-block; height:100px;
display:none hides the element, with all its children
float:left float:right styles are used especially with images to allow them to float at the side while the text flows around them, like in a newspaper (typographic). See codepen example
Whatever typography options are available a Text editor, are also available in CSS. And more!
Text aligns to baseline, UI is often better aligned to the top
The default HTML behavior is to align elements in a <div> row to the bottom (actually to the text baseline), because this is the default in typography:
The Size may Vary
This default does not work well for user interface. Consider this HTML/JSX. Size 2 (two options visible) makes the SELECT “taller”
<div class="debug">
<button>click me!</button>
<input placeholder="type here please!" />� <select size="2">� <option>My way</option>� <option>The highway</option>� </select>
</div>
One solution is to use vertical-align:top in the surrounding DIV
baseline
CSS in developer tools. CSS Box model
See also margin collapsing (happens on top or bottom of adjacent boxes)
CSS in JSX
Simply indicate the class (or directly style) for your HTML element. They can be expressions class={some JS expression}
class="class1 class2 class3" works in HTML so your JS expression above can also return a space-separated list of class names
React accepts both class and className but recommends className and warns exactly once about class. You can ignore that warning in the lab!
Please use class in the lab (so your JSX works with Vue as well) and className in the project (if you work with React)
Inline styles (which are discouraged because they cannot be reused, like classes can) cannot be strings in React. For example instead of style="float:right;width=50%" you need to write the general form, also accepted by Vue: style={{float:"left", width:"50%"}}
{} is an object creation literal, so you can also write style={someObject}
DOM (native) events
clicks, keys, change, input, focus, blur
Event handler callback
function myEventPrinterACB(anEvent){ console.log(anEvent); }
..
<button onClick={myEventPrinterACB} />
<input onInput={myEventPrinterACB} />
Whenever the user clicks the button, or inputs something in the textbox, myEventPrinterACB will be invoked!
JS remark: In this case, console.log is a function itself, so we can just pass it as callback!
<input onChange={console.log} />
React treats onChange and onInput events identically !
In DOM and Vue, INPUT onChange fires only when changing the focus to another component,
while onInput fires at every keystroke, cut, paste…
Event target. DOM Element properties
function targetPrinterACB(someEvent){ console.log(somEvent.target); }
..
<button onClick={targetPrinterACB} />
<input onChange={targetPrinterACB} />
target is an event JS object property that reveals the HTML Element where the event took place. In this case it’s the BUTTON or the INPUT box. It’s actually the JavaScript object that represents the INPUT box in the browser model of the HTML page, i.e. the DOM (Document Object Model).
INPUT DOM objects have a property called value. It always contains a String storing the current INPUT box content.
function valuePrinterACB(evt){ console.log(evt.target.value); }
<input onChange={valuePrinterACB} />
TW1.4 Handle native events
number
SidebarView
disabled
click
src/views/sidebarView.js
Element | Native event name | Parameter type | Comments/hints |
Both + and - buttons | onClick | Event (typically not used for clicks) | The two initial event handlers (CB or ACB?) should print different messages on the Console. For example, print the desired number (number +1 for the + button, number -1 for the - button). Watch the Console when pressing the buttons Hint: nest the event handler inside the component function. You will need that later. |
Callback checklist onEvent={ACB} addEventListener(event, ACB)
How to pass the callback? <someHTMLTag onClick={acb} /> Other params needed besides the callback? No
someDOMNode.addEventListener("click", cb) Other params needed? Yes, "click"
What does <someHTMLTag onClick={acb} /> return? nothing
When does <someHTMLTag onClick={acb} /> return? Immediately. (just makes a plan that when user clicks, ACB will be invoked)
Who calls the callback, and sends parameters? browser event loop
Role (and usual name?) of callback parameters event object (includes target, timestamp, etc, details later)
What is the callback expected to return? nothing any longer. false prevented default browser action (e.g. submit form)
When is the callback called? When user interacts,
or e.g. page load: document.body.addEventListener("load”, cb)
Is the callback called once? Or repeatedly? As many times as the user interacts
What is the role of the callback? Programmatically react to user interaction
Example callback names buttonClickedACB
Frameworks may add to the default browser mechanism/event object but the principle is the same
Callback timeline
sort(CB) �call
0 or more CB(a,b) invocations by sort()
sort(CB) �returns
From this point on, it is guaranteed that CB will not be invoked by this run of sort(CB) because sort(CB) has ended!
onClick={ACB} or �addEventListener(click, ACB)�return immediately
0 or more ACB(event) invocations by the browser / JS environment
From this point on, it ACB will not be invoked any longer because it was unsubscribed
removeEventListener(ACB)�
Synchronous: sort, map, filter, reduce
Asynchronous: setTimeout/clearTimeout, native events, custom events, promises, observers, functional custom components, component lifecycle
t
t
Note: onEvent= subscriptions are never explicitly unsubscribed. The browser ensures unsubscription if the target element (native component) is removed from the DOM tree
Native event bubbling, capturing, low-level vs higher-level, debouncing
Event Bubbling and Capturing
function targetPrinterACB(e){console.log(evt.target);}
<div onClick={targetPrinterACB}>
<button onClick={targetPrinterACB} >
click me!
</button>
</div>
The clicks on the button are detected by its div parent! And parent of parent, etc.
The parent handler (callback) is called after the child handler. It means that the event moves “bubbles” up the tree. Not all events bubble. See evt.bubbles
The target is always the DOM Element where the event actually occurred.
In capture mode the parent detects the event before the target is notified
<div onClickCapture={targetPrinterACB}>..</div>
Any event handler can call evt.stopPropagation() to prevent the event from propagating further up or down the tree.
More ways to produce click on a button
In most GUI technologies
Thus:
Other high level: keypress (keydown+keyup) etc
The high-level event is sometimes fired just before the low-level event that produces it:
68
Interesting Event object properties and methods
target | HTML element (native component) on which the event took place. Important for getting the value of a textbox, date element etc. |
timeStamp | timestamp when the event was fired |
altKey, ctrlKey, shiftKey, metaKey | special keys pressed when the event took place |
pageX, pageY, offsetX, offsetY, screenX, screenY | mouse coordinates relative to page, target, screen |
bubbles | whether the event bubbles or not |
keyCode | for key events like keyDown, keyUp, keyTyped. Enter’s keyCode is 13 |
preventDefault() | stops the default browser action (e.g. opening another page after click on a link) |
stopPropagation() | stops event propagation (capturing and/or bubbling), no other listener will be called! |
cancellable | can the event be cancelled with preventDefault()? |
Debouncing
Problem: performWebSearch() is an expensive operation, we don’t want it to happen very frequently (it may cost money $$$! Or just take a very long time).
The solution is called debouncing. Below we make sure that at most two such calls per second are performed (500 ms minimum between them)
function doWorkACB(){ performWebSearch() /* $$$ */; }
<button onClick={ debounce( doWorkACB, 500) }
>Search!</button>
debounce(ACB) is not built-into browsers but it is pretty easy to implement debounce with setTimeout and clearTimeout, see an example here.
Or you can use a library such as lodash.
Read more about debouncing and throttling.
JSX custom events
Custom Event: a callback prop. Fire/emit cust. ev.
Typically, whoever uses a custom component is not interested in DOM events, because they are too low level. When an input box changes we are interested in the new text and not in the event object.
<SomeView onTextChange={someACB} /> // for custom event handling, see coming slides
// someView.js:
function SomeView(props){
function inputHandlerACB(evt){props.onTextChange(evt.target.value); } // fire custom event
return <input onInput={inputHandlerACB} /> ;
}
SomeView can be re-organized to receive the text differently, e.g. through a multi-line TEXTAREA, which may produce different DOM events. The code that uses MyView is not affected by this UI change. In other words, the module responsible for rendering UI (MyView) is also responsible for:
Remember the callback receive rule: When we receive a callback, we are supposed to call when suitable (according to the logic you program), and are responsible to set its arguments! Custom event firing (TW1), React state setter (TW3)
The value of named callbacks. Native event handlers must be ACB
SidearView table content‣
TypeError: props.deleteDish is not a function // looks like custom event, shouldn’t be fired at render time
at dishesTableRowCB (test.js:130:22)
at Array.map (<anonymous>)
at SidebarView (test.js:146:177)
<button onClick={props.deleteDish(dish)}>x</button>
Correct (but we still have to pass the deleteDish prop!)
<button onClick={xClickedACB}>x</button>
...
function xClickedACB(evt){ // evt is often ignored for click
props.deleteDish(dish);
}
Q Where does dish come from?
A: xClickedACB must be defined nested within the (dish) array rendering callback!
TW1.4 Native events and firing custom events
number
SidebarView
disabled
click
onNumberChange
Custom event fired by the custom component (view in this case)
Native element(s)
Native event fired by the native component (element)
Graphical appearance (sketch)
src/views/sidebarView.js
Link to this slide guide
TW1.4 Handle native event, fire custom event
number
SidebarView
disabled
<a />
src/views/sidebarView.js
choose name!
click
Element | Native event handler | Parameter type | Fires custom event | Custom event parameter(s) type | Custom event parameter meaning | Comments |
hyperlink �<a /> on dish name | onClick | Event | choose a name which indicates that the user is interested in a dish | object (dish) | The dish which the user is interested in and wants to see more info about | Repeat the previous steps to fire a custom event when the dish name hyperlink is clicked. Each dish will have its own click event handler, therefore the handler callback must be defined nested inside the array rendering callback. You can start by simply logging the dish. Then you can fire a custom event, as in the previous step. If you do, the UI test will figure out its name, and print the dish when it receives the custom event. |
Prints custom event name and the dish object
Handling a custom event with a callback
// in another component (presenter)
function gotTextACB(txt){ console.log(txt); }
—-------------------------------------------------------------------------------
<CustomComponent customEvent= {cb} /> Custom
<SomeView onTextChange= { gotTextACB } />
—-------------------------------------------------------------------------------
<input onInput= {inputHandlerACB} /> Native
<nativeComponent nativeEvent= {cb} />
—-------------------------------------------------------------------------------
Custom events ...
Both custom and native events are referred to as just events in a lot of documentation sites. But the distinction is important
“Props down, events up”
Data flow in the JSX tree
<button />
disabled
click
<input />
value
input
<MyComponent />
cars
onTextChange
<button />
disabled
click
number
onNumberChange
<SidebarView/>
<Sidebar/>
model
TW1.5 Presenter handles custom events, changes Model
tw/tw1.5.2.js Vue
src/vuejs/sidebarPresenter.js
Link to this slide guide
onNumberChange
SidebarView
Sidebar
number
DinnerModel
Presenter changes Model from custom event handler
Custom event fired by View, handled by Presenter
model
TW1.5 Presenter handles custom events, changes Model
tw/tw1.5.2.js Vue
src/vuejs/sidebarPresenter.js
onNumberChange
SidebarView
Sidebar
number
DinnerModel
model
Custom event to be handled | Parameter Type | Comments/hints |
onNumberChange | integer | <SidebarView .. onNumberChange={someACB} ../> In someACB, call DinnerModel method to set the number of people |
Name you chose for user expressing interest in dish | dish | Pass one more ACB to the View. It should call the DinnerModel method to set the current dish |
Name you chose for user wanting to remove dish | dish | Call DinnerModel method to remove dish |
Add the Sidebar before Summary in the App!�src/views/app.js
At the Console:
myModel // will print { numberOfGuests:2, dishes:[..] }
myModel.setNumberOfGuests(7) // you should see the number changing in both Sidebar and Summary views!
Callback checklist customEvent={ACB}
How to pass the callback? <SomeComponent customEvent={ACB} /> Other params needed? No
What does <SomeComponent customEvent={ACB} /> return? nothing
When does <SomeComponent customEvent={ACB} /> return? Immediately (just makes a plan that when the custom event is fired, ACB will be invoked)
Who calls the callback, and sends parameters? SomeComponent code, typically in a native or custom event listener
Role (and usual name?) of callback parameters Can vary. Typically abstract (non-graphical objects)
What is the callback expected to return? nothing
When is the callback called? Typically when a native or custom event handler invoked in SomeComponent
Is the callback called once? Or repeatedly? Repeatedly
What is the role of the callback? Interpret a lower level event in more abstract terms
Example callback names somethingHappenedACB
Synchronicity is a relative matter
Handling a native event is asynchronous in relation to the rendering: the ACB defined in the View will be invoked later, zero or more times.
Handling a custom event is also asynchronous in relation to rendering: the ACB defined in the Presenter will be invoked later, zero or more times.
However, the following are synchronous in relation to each other
handling a native event -> fire a custom event -> handle the custom event
In other words, handling the native event does not end until the custom event it fires has been handled.
We still use ACB in naming of the native event handler and custom event handler, because they are asynchronous in relation to e.g. rendering.
Render → Handle Events → Update (re-Render)...
Now you know how to Render and to Handle Events
What is Update?
Update in JSX means that you change the props of the components
After that, they should be re-rendered by the framework
Typically the props of components (Views) are changed based on the Data in the Model
The changes are done by the Presenter
Admin messages
Please find a lab partner and join a TW2_TW3 group ASAP! We will create your git repo for the lab group
Presentation after each TW:
Bonus points for properly reported issues.
Architectures: MVP, MVC, MV-VM
DH2642
Most concepts presented here are valid for most Graphical User Interface technologies and can be implemented in many ways.
Given two representations of the same data, where to keep the Data??
Unexperienced interaction programmers tend to keep data in both representations, ending up with a lot of interdependency
85
Observer-Observable (Subject)
Graphical (usually) representations observe the Data Model. They are called Observers
Model is also called Observable.
New Observers can be added at any time to an Observable. They can also be removed.
Observer-Observable is known as “the Observer pattern”
86
DinnerModel
numberOfGuests
addObserver
notify
addObserver
notify
setNumberOfGuests()
callback
call
In Vue, addObserver and notify are performed in the framework (proxy). In React we call them explicitly.
You will implement Observer in Week 3
View-Presenter separation
The graphical representations now need to
The graphical representations now need to
87
DinnerModel
numberOfGuests
addObserver
addObserver
notify
setNumberOfGuests()
callback
call
notify
In Vue, addObserver and notify are performed in the framework (proxy). In React we call them explicitly.
Model-View-Presenter
88
DinnerModel
numberOfGuests
addObserver/proxy
notify
addObs/proxy
notify
NumberPresenter
SummaryPresenter
render(number)
onNumberChange
NumberEditor
SummaryView
render(persons)
setNumberOfGuests()
callback
call
click
Views are aware of the application use case to different degrees. A NumberEditor can be generic, while the SummaryView is more tied to the dinner use case. Views do not assume anything about Model.
In Vue, addObserver and notify are performed in the framework (proxy). In React we call them explicitly.
It is the job of the Presenter to mediate between Model and View: adapt the data formats to props expected by the View, transform (or not) custom events into Model changes etc.
Test Model → Presenter notification
In tw1.5.2, place breakpoints in both Summary presenter and Sidebar presenter
At the Console, change the Model
myModel.setNumberOfGuests(7)
Both Summary and Sidebar presenter breakpoints should fire.
What makes this work?
number
onNumberChange
SidebarView
Sidebar
people
SummaryView
Summary
DinnerModel
update!
update!
change Model (set nr guests)
Model and Views do not make any assumption about the outside world.
They only get parameters and fire notifications.
Presenters are supposed to connect them
Presenters own/contain Views
Presenters can be implemented as Components but they should not render any UI (or very little). One Presenter can combine several Views
Views are contained into Presenters, only one Presenter should know/care about a View instance therefore Views are “lower down” in the hierarchy
Data (like number) travel down from Presenter to View. Props down
Custom events (like onNumberChange) notifications travel up from View to Presenter. Events up
Presenter components are often called Containers, and Views are known as Presentational :) components. See React evangelist older text.
91
Model-View-Controller
MVC terminology will be recognized by most interaction programmers
One MVC View can have more controllers depending on its mode (e.g. graphical editor: same event, different model semantic)
In MVP
M→V→C→M is circular�M←→P←→V is linear
Most component-oriented architectures end up with MVP because the component hierarchy requires a linear relationship between P components and V components
92
DinnerModel
numberOfGuests
addObserver
notify
callback
call
Controller
setNumberOfGuests()
View
show()
hide()
addEventListener()
event
MVC in the History of interactive computing
MVC was introduced by Trygve Reenskaug (1979) at Xerox Palo Alto Research Center (PARC)
With MVC and the Smalltalk language, PARC achieved one of the first WIMP (windows, icons, menus, pointers) GUIs: Xerox Star
In the 1980s the Star inspired Apple Lisa and the slightly more famous Apple Macintosh. Microsoft Windows was designed with the help of former PARC engineers.
Other PARC inventions that we use today: Ethernet, PostScript, laser printer, WYSIWYG editors, bitmap graphics,...
93
Model-View-ViewModel
MV-VM can be regarded as a MVP variant.
The difference is that there is data binding between View and Presenter (ViewModel)
Data binding examples in Vue or Angular: The View is composed by a “template string”
Vue translates its templates to JSX nowadays. JSX is much more flexible than template strings.
Before React and JSX, templates and binding were the main declarative update mechanism in Web frameworks. MV-VM and bindings are still used in Swift.
MVVM was introduced by Microsoft in 2005 for e.g. Silverlight. Many other technologies support binding (e.g. JavaFX, Swift)
94
Technology evolution at Model/Application State level
One would think that the Model (abstract data) level is just an object in a given programming language. But see the model part of pokemon-explorer !
Reactive programming: ReactiveX (used by Angular, RxJS). RxJava, RxPy �Everything is an Observable (from Model to remote data access, and even DOM events)
For large applications, and lots of programmers, managing application state can become unscalable
=> State managers: Flux, Redux, Recoil (by React), Vuex, Pinia (by Vue) model + other state data
Frameworks work at Presenter-View level �Redux, Vuex, RxJS work mostly at abstract data level� => one can use any of them with any framework (e.g. even Vuex with React…) � Or even with no-framework (e.g. the course hyperscript using Redux, etc)
95
Layout with CSS
flexbox, grid
Layout: motivation
Interaction Programming often wants to create flexible containers, where different parts take various amounts of space according to their importance.
When the container is enlarged, the most important parts should take most growth.
When the container is reduced, the most important parts should remain visible, and the others can be accessible via scrolling, clicking, etc.
In CSS such behavior can be achieved with flexbox and grid
The most important part of this layout is the list of files. It takes all the horizontal and vertical growth.
Note also how the most important buttons are visible at all times.
On small sizes, certain parts are hidden. On Web apps, this can be achieved with CSS media queries.
Layout CSS: flexbox
Vertical flexbox example
Horizontal flexbox used in lab
Combined horizontal and vertical flex (“border layout”)
You can achieve similar layout with grid. See “border layout” grid example.
Web APIs
synchronous vs asynchronous, fetch(), Promises, elements of HTTP, some component lifecycle
Issues with Synchronous Web API access
Synchronous web API invocation (pseudocode!)
const data=getDataFromAPI("http://api.server/endpoint", apiParameters)
This may be an intuitive function call (remote procedure call, RPC). But getDataFromAPI blocks until the server responds.
Also how about network/server errors? They could be sent as return value, or can be detected using catch ...
The problem with this approach is that we don’t know how long getDataFromAPI will take. This can seriously hamper user experience.
function getDataCB(e){ getDataFromAPI("http://some.api", apiParameters); }
<button onClick={getDataCB}>Search!</button>
When the button is clicked, if getDataFromAPI takes 5 seconds to return, the UI (browser tab…) will be frozen (unresponsive) for 5 seconds!
Never take too long in your event listeners! All event listeners run in the (e.g. browser) event loop. If you need to perform time-consuming operations, you can use e.g.
setTimeout(function longTimeACB(){ time ; consuming ; operations; }, 0); // 0 is the delay in ms
You can perform synchronous HTTP accesses (not recommended!) using XMLHttpRequest with specific parameters.
Asynchronous Web API access
getDataFromAPI("http://api.server", apiParameters, successCallbackACB, errorCallbackACB)
The result is a parameter sent to successCallbackACB
getDataFromAPI("http://api.server", apiParameters , function successACB(data){ ..}
, function errorACB(error){..})
Conceptually similar to UI events! onSuccess onFailure...
The advantage is that getDataFromAPI returns immediately so the UI (browser tab) is not frozen if used e.g. in an event listener (or in code called by the event listener, custom event handler, etc).
Returns immediately= just makes a plan that when the web API will return successCallbackACB will be invoked if all goes OK and errorCallbackACB will be invoked if something goes wrong (network, bad parameters to the API etc)
The Callback Pyramid of Doom
The “success and error callbacks” pattern (implemented in browsers by e.g. XMLHttpRequest aka AJAX) was really popular until people run into another problem.
What if in successCallback we want to make another API call whose parameters depend on the first? And then another?
getDataFromAPI("http://api1.server", apiParameters,
function firstOperationACB(data1) {
getDataFromAPI("http://api2.server", apiParametersUsingdata1,
function secondStepACB(data2) {
getDataFromAPI("http://api3.server", apiParametersUsingdata1_data2,
function thirdStepACB(data3){
/* use data1, data2, data3 */
},
function thirdStepErrorACB(error3){..}.
)
},
function secondStepErrorACB(error2){..}
)
},
function firstStepErrorACB(error1){..}
)
Asynchronous Web API access in browsers: fetch
getDataFromAPI("http://api.server", apiParameters, successCallbackACB, errorCallbackACB)
The modern approach is to return a Promise and promise chains
fetch("http://api.server", parameters).then(successCallbackACB).catch(errorCallbackACB)
The object returned by fetch() is of type Promise
catch will detect all errors in the promise chain so far.
fetch("http://api.server", parameters).then(acb1).then(acb2).catch(errorCallbackACB)
How the promise chain works:
No more pyramid!
fetch(), fetch().then(), etc return immediately. We basically set up a mechanism/plan that will set in motion later, when the network response comes.
Also return immediately: setTimeout(), addEventListener(), onClick=... etc
HTTP example
> telnet standup.eecs.kth.se 8080
Trying 130.237.225.65…
Connected to standup.csc.kth.se.
Escape character is '^]'.
GET /iprog/last?day HTTP/1.1
Host:standup.eecs.kth.se:8080
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: *
Content-Type: application/json
Content-Length: 414
Date: Fri, 29 Jan 2021 15:18:17 GMT
{"DH2642_info":[
{"grp":16, "cnt":806},
…
{"grp":7, "cnt":1},
{"grp":123, "cnt":1},
{"grp":29, "cnt":1}]}Connection closed by foreign host.
telnet is a Unix/Mac terminal command. On Windows one can also try nc or socat
telnet output
.
TCP socket is ready! Now we can type the HTTP Request
Request method path query string protocol vers.
Request header
Empty line: end of Request headers
Protocol version, Response code status
Response header
Response header
Response header
Response header
Response header
// Empty line: end of Response headers
Response body (see Content-Length)
server waits a bit for further HTTP requests, then closes TCP socket
fetch : two promises! JSON, serialization, deserialization
function processHTTPResponseACB(response){return response.json();} // promise 2
function processHTTPContentACB(data){ console.log(data); }
fetch("https://pokeapi.co/api/v2/pokemon/1") // promise 1
.then(processHTTPResponseACB)
.then(processHTTPContentACB)
.catch(function processErrorACB(err){ console.error(err); })
Fetch only “promises” that a response will come. The response contains
To get the actual data, a few methods of the response can be called. All these methods return Promise so they must be immediately followed by then() !
The most used response method is json() which assumes that the data is represented as JSON (JavaScript Object Notation) which is a string like
'{ "propertyName": "value", "propertyName2": "value 2" } '�
To make such a string yourself (e.g. when sending data to an API) you can use JSON.stringify(someObject). This is called serialization
To read an object from a string, you can use JSON.parse(someJSONString) . This is called deserialization
response.json() can be written as response.text().then(function parseACB(txt){return JSON.parse(txt); })
Callback checklist fetch(..).then(ACB)....catch(EACB)
How to pass the callback? fetch(..).then(ACB) catch is optional and may come later in the promise chain. Other params needed? No
What does fetch(..).then(ACB) return? A Promise, which in turn has a then(..) method, catch(..) etc.
When does fetch(..).then(ACB) return? Immediately. Just makes a plan that when the HTTP response comes, ACB will be invoked
Who calls the ACB callback, and sends parameters? Browser network layer. See the Network tab in DevTools
Role (and usual name?) of callback parameters The HTTP response: response code, headers. No data !
What is the callback expected to return? Typically response.json() or response.text()
That is, a new promise is returned, to treat the data content of the HTTP access.
When is the callback called? When the HTTP response has arrived from the server. The data may still be in transit!
Is the callback called once? Or repeatedly? Once or zero times (if there is an error in the HTTP call, EACB is called)
What is the role of the callback? Look at the response and fire up the second fetch promise like json() or text(). Examine the response code, possibly throw if it is not 200 (HTTP OK). Then the EACB will be called
Example callback names treatHTTPResonseACB
Callback checklist response.json().then(ACB)....catch(EACB)
How to pass the callback? response.json().then(ACB) Other params needed? No
fetch(..).then(function treatResponseACB(response){ return response.json(); }).then(ACB)
What does response.json().then(ACB) return? A Promise, which in turn has a then(..) method, catch(..) etc.
When does response.json().then(ACB) return? Immediately. Just makes a plan that when the HTTP body comes, ACB will be invoked
Who calls the ACB callback, and sends parameters? Browser network layer.
Role (and usual name?) of callback parameters The data (response body) part of the HTTP access (after the empty line), interpreted as JSON
What is the callback expected to return? Nothing specific, typically store the data in some object. �But you can return some processing of the data if you have a then(ACB).then(CB). Note that the CB is synchronous! �Or you can return a promise (maye a new fetch() based on the data you just got?), then(ACB).then(ACB2)
When is the callback called? When the HTTP data has arrived from the server (second part of the HTTP protocol)
Is the callback called once? Or repeatedly? Once or zero times (if there is an error in the HTTP call, EACB is called)
What is the role of the callback? Process the HTTP response data
Example callback names treatHTTPDataACB
Returning a promise and treating HTTP errors
function myAPICall(apiParams){
function treatHTTPResponseACB(response){
if(!response.ok) throw new Error("API problem "+response.status); // or response.status!==200
return response.json();
}
return fetch(API_ROOT+apiParams).then(treatHTTPResponseACB);
}
There is no catch. This is on purpose, to give the code invoking myAPICall to deal with the error as it sees fit.
myAPICall(params).then(treatDataACB).catch(treatErrorACB)
Or just do some processing, and return a promise, often without catch, letting the code which calls our function to do the catch:
return myAPICall(params).then(someProcessingACB)
Treating HTTP errors in more detail (optional)
Even for non-200 responses, the HTTP data (content) often contains an explanation of the error.
response.text() and response.json() are promises, so they have a then()... The previous version can be written like this:
function myAPICall(apiParams){
function treatHTTPResponseACB(response){
function throwExplanationACB(errorExplanation){
throw new Error("API error "+ response.status+" "+errorExplanation);
}
if(!response.ok)
return response.text().then(throwExplanationACB); // text() will resolve to explanation!
return response.json();
}
return fetch(API_ROOT+apiParams).then(treatHTTPResponseACB);
// again, no catch, we let the caller of myAPICall catch and treat the error
}
Example of using generic API call
const API_ROOT= "https://pokeapi.co/api/v2/";
function myAPICall(apiParams){
function treatHTTPResponseACB(response){
if(!response.ok) throw new Error("API problem "+response.status);
// or response.status!==200
return response.json();
}
return fetch(API_ROOT+apiParams).then(treatHTTPResponseACB);
}
function getPokemonPromise(pokemonId){
return myAPICall("pokemon/"+pokemonId);
}
Promise result processing
Sometimes we don’t need the whole result of an API, or we need it in another form.
Instead of an object, we may want our promise to resolve to an array, for example the array of Pokemon moves.
function transformResultACB(pokemon){ // an object
return pokemon.moves; // an array!
}
function getPokemonMovesPromise(pokemonId){
return myAPICall("pokemon/"+pokemonId).then(transformResultACB);
}
Or:
function getPokemonMovesPromise(pokemonId){
return getPokemonPromise(pokemonId).then(transformResultACB);
}
async/await
Special JavaScript (and C#) syntax for Promises.
The following two functions are equivalent:
function getPokemonPromise(pokemonId){
return fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId) // promise 1
.then(function processResponseACB(response) { return response.json();} ) // promise 2
}
async function getPokemonPromise(pokemonId){ // returns Promise!
const response= await fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId) ; // promise 1
const content= await response.json(); // promise 2
return content;
}
// test at Console:
getPokemonPromise(25).then(console.log)
console.log(await getPokemonPromise(25))
await getPokemonPromise(25)
OBS: calling an async function without await is legal but most probably not what the programmer wants! It will simply initiate the promise but not wait for it to resolve! This is a frequent mistake, leading to race conditions.
async/await eliminates the use of some callbacks. But callbacks are very important for Interaction Programming, hence all course material uses .then(dataACB).catch(errorACB)
Passing parameters to APIs
Parameter format differs from one API to the other. Read the API documentation (example: Pokemon API with endpoints: Pokemon , Items etc).
Documentation / API behavior can change!
Parameters use a number of HTTP terms which you need to be aware of.
Authentication to the API (e.g. sending the API key) is usually done via query string or headers
Both the HTTP Request and the Response can contain headers and body (data)
Examine HTTP accesses in the Network tab of the Developer Tools. Check the method and URI under Headers/General, then Headers/Response, Headers/Request, etc
HTTP method and REST. API Endpoints
HTTP method is used by a convention called REST (REpresentational State Transfer) to decide the API action:
Note that the resource path (and thus the whole URL) can be the same for e.g. a GET, PUT, or DELETE.
The HTTP method tells whether we read the object (GET), update it (PUT) or remove it (DELETE)
fetch("http://api.server/resource/12445" ,{ method: "GET" }).then(...)
fetch("http://api.server/resource/12445" ) // GET is default!
fetch("http://api.server/resource/12445" ,{ method: "DELETE" })
fetch("http://api.server/resource/12445" ,{ method: "PUT" , body: JSON.stringify(someObject)})
The API Endpoint is thus given by method + resource path
Query string
Comes at the end of the resource path
cleverDishSearch?freeText=bla+bla&type=appetizer
? followed by name=value pairs separated by &
Names and values must be escaped so they not disturb the HTTP communication. For example spaces are transformed in + or %20.
To avoid all escaping complexities, use the built-in class URLSearchParams, pass a JavaScript object to its constructor.
fetch(API_SERVER+
"/cleverDishSearch?" + new URLSearchParams({ freeText:"bla bla", type:"appetizer" })
) // GET HTTP method is default!
If the value of a URL parameter is an array, URLSearchParams will represent it correctly:
{urlParam: [1,3,4], }
urlParam=1,3,4 or urlParam=1%2C2%2C4
Historically query strings are produced by HTML forms when the Submit button is pressed.
HTTP headers and body (data sent to/from server)
Headers are key-value pairs (like the query string but in a different format)
With fetch(), send headers like a JavaScript object.
We place the data as a JSON string in the body property of the fetch() second parameter
fetch('https://api.server/endpoint', {
method: 'POST', // or 'PUT'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(someObject),
}).then...
The server response also contains headers. You can retrieve them from the fetch() response.
fetch("https://pokeapi.co/api/v2/pokemon/1").then(
function responseACB(r){ r.headers.forEach( function headerCB(h){console.log(h);} ); }
);
Call an API endpoint (lab table preview)
Goal: Search dishes | | Hints |
HTTP method | GET | |
HTTP resource path | Find it in the API documentation | Find the simplest endpoint that searches dishes and is not deprecated |
query string | Find it in the API documentation | The dish type and a text query. Use URLSearchParams! |
HTTP headers | API key | Was supplied to you |
HTTP body (data) | none | |
Return array of objects, containing dish IDs and names | The API will return an object with some meta-data. One of the properties of that object is an array of objects, each containing an ID, a dish name etc. Return that array only! |
async/await
Special JavaScript (and C#) syntax for Promises.
The following two functions are equivalent:
// without async/await
function getPokemonPromise(pokemonId){
return fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId) // promise 1
.then(function processResponseACB(response) { return response.json();} ) // promise 2
}
// with async/await
async function getPokemonPromise(pokemonId){ // returns Promise!
const response= await fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId) ; // promise 1
const content= await response.json(); // promise 2
return content;
}
// test at Console:
getPokemonPromise(25).then(console.log)
console.log(await getPokemonPromise(25))
await getPokemonPromise(25)
async/await eliminates the use of some callbacks. But callbacks are very important for Interaction Programming, hence all course material uses .then(dataACB).catch(errorACB)
Promises and User Interaction
Dealing with async data in interactive applications (suspense)
Promise state
{promise, data, error} A JavaScript object with typically 3 properties
When promise changes, data and error are set to null, waiting for the promise to resolve or to reject
promise changes due to user interaction (a new search, reading another dish)
When anything in the promise state changes, the UI will update
However the Promise state, and its issues, are similar.
Naïve version. DO NOT USE!
function resolvePromiseNaiveDontUse(promiseToResolve, promiseState){
promiseState.promise=promiseToResolve;
promiseState.data= null; // UI update! The user does not keep seeing results from previous search
promiseState.error= null;
function saveDataACB(result){ promiseState.data= result; } // triggers UI update because of changing state
function saveErrorACB(err) { promiseState.error= err; } // triggers UI update because of changing state
promiseToResolve.then(saveDataACB).catch(saveErrorACB);
}
function AsyncPresenter(props){
function doSearchACB(params){ resolvePromise(myAPICall(params), props.model.myPromiseState); }
return <SomeView onSearch={doSearchACB} searchResults={props.model.myPromiseState.data} />
}
function SomeView(props){
function handleInputACB(event){ props.onSearch(event.target.value); }
return <div><input onChange={handleInputACB} /> { props.searchResults }</div>;
}
You get some variant of this in almost every tutorial. But it has an essential shortcoming
Problems with the naïve setup: race condition
A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.
Example: Second promise can resolve before the first promise. Server happens to have less workload, network glitches, etc.
Optimistic timeline (milliseconds):
000 doSearchACB("past")
001 promiseState.data=null, fetch("?past")
007 doSearchACB("pastic")
008 promiseState.data=null, fetch("?pastic")
500 promiseState.data= "lots of pasta"
700 promiseState.data= "pasticcio"
User sees “pasta” for 200ms, then “pasticcio”. Fair enough.
“Shit happens” timeline:
000 doSearchACB("past")
001 promiseState.data=null, fetch("?past")
007 doSearchACB("pastic")
008 promiseState.data=null, fetch("?pastic")
700 promiseState.data= "pasticcio"
800 promiseState.data= "lots of pasta"
User sees “pasticcio” for 100ms (if they manage), then “pasta”, which is not what they were looking for…
Remarks
Does debouncing resolve this problem? Debouncing would ensure that
So the problem may be alleviated somewhat but not strictly solved
The root of this problem is shared data
Solution: only save if promiseState.promise is still the same
function resolvePromise(promiseToResolve, promiseState){
promiseState.promise=promiseToResolve;
promiseState.data= null;
promiseState.error= null;
function saveDataACB(result){
if(promiseState.promise !== promiseToResolve) return;
/* TODO save result in promiseState, as before */
}
function saveErrorACB(err) {
/* TODO same check as above */
/* TODO save err in promiseState, as before */
}
promiseToResolve.then(saveDataACB).catch(saveErrorACB);
}
Avoiding the race condition
Optimistic timeline:
000 doSearchACB("past")
001 promiseState.promise=promise1;
fetch("?past")
007 doSearchACB("pastic")
008 promiseState.promise=promise2;
fetch("?pastic")
500 promiseState.data= "lots of pasta"
700 promiseState.data= "pasticcio"
User sees only “pasticcio”
Realistic timeline:
000 doSearchACB("past")
001 promiseState.promise=promise1;
fetch("?past")
007 doSearchACB("pastic")
008 promiseState.promise=promise2;
fetch("?pastic")
700 promiseState.data= "pasticcio"
800 promiseState.data= "lots of pasta"
User sees only “pasticcio”
“lots of pasta” is lost in garbage collection because no object refers it after the first saveDataACB ends.
A waste of resources, we fetch data just to dump it. See cancelling promises in next slide.
Cancel the old promise (optional)
The promise may retrieve a huge MP4, eating Cellular Data $$$. See documentation about cancelling promises (AbortController)
function resolvePromise(promise, promiseState){
if(promiseState.promise
&& !promiseState.data && !promiseState.error) // not yet resolved/rejected
cancelPromise(promiseState.promise);
promiseState.promise=promise;
promiseState.data= null;
promiseState.error= null;
function saveDataACB(result){
if(promiseState.promise!==promise) return;
/* TODO save result in promiseState, as before */
}
function saveErrorACB(err) {
/* TODO same check as above */
/* TODO save err in promiseState, as before */
}
promise.then(saveDataACB).catch(saveErrorACB);
}
MDN Note: When abort() is called, the fetch() promise rejects with a DOMException named AbortError.
But: Since the this[propertyName] is replaced by a new object, that error will be simply garbage-collected.
Promise states in the lab (stored in the model)
Promise state | Purpose | hints |
searchResultsPromiseState | The promise state of the latest searchDishes promise | From a Presenter, access as props.model.searchResultsPromiseState |
currentDishPromiseState | The promise state of the latest getDishDetails promise | |
How about user experience when waiting for the promise to resolve?
function AsyncPresenter(props){
return <SomeView onSearch={doSearchACB}
searchResults={ props.model.searchPromiseState.data}/>;
}
searchPromiseState.data may be null. The View would need to check that… Alternatively props.model.searchPromiseState.data && <SomeView ../>
When the user interacts and produces onChange events in the <input /> box, which lead to onSearch custom events, props.model.searchPromiseState.data is undefined for 500ms, 2s… The user sees no feedback to their interaction.
Basic usability principles: visibility of system status, provide feedback to user actions
Suspense: promiseNoData(promiseState) || <View />
Presenter:
return <div>
<SomeViewForm onSearch={doSearchACB} />
{ promiseNoData(props.model.searchPromiseState) ||
<SomeViewResults searchResults={props.model.searchPromiseState.data}/>}
</div>;
Normally Presenters never generate UI. However in this case a root needs to be returned, hence the root DIV. React has React.Fragment for this use case (multi-root JSX). There are corresponding techniques also in Vue. DIVs are OK in the lab for this purpose.
promiseState.promise | promiseState.�data | promiseState.�error | promiseNoData() return value |
falsy | don’t care | don’t care | <div>No data</div> |
truthy | falsy | falsy | <img src=TODO class=TODO /> i.e. a “loading image” animated GIF |
truthy | falsy | truthy | <div class=TODO >{error}</div> |
truthy | truthy | falsy | false . This will ensure that the second || operand is evaluated: there is data, so the View can render that data. Note that promiseNoData returns truthy in all other cases above! |
Initial promise state
To show some initial data to the user, some Presenters need to initiate a promise at first render, if there is no promise yet
function AsyncPresenter(props){
if(!props.model.somePromiseState.promise) {
props.model.doSearch({}) // does a resolvePromise internally. The unit test assumes doSearch()
// will execute only once! After that, somePromiseState.promise will be truthy
}
return promiseNoData(props.model.somePromiseState) || <SomeView someProp=../>;
}
If the condition is not correctly checked, or if somePromiseState.promise is not set to truthy, this can lead to an infinite re-render (because state changes on each render, leading to another render). That will lead to an infinite number of API calls. Keep an eye on the Developer Tools Network tab!
The more rigorous way to address this is to trigger the promise using component lifecycle (TW3).
Labs: aim to get examined Tue & Wed. Book 5 min!
Join a Project group. Start thinking of project, API
1.5, 2.5 Native event handler(ACB) -> customEvent -> customEvent handler(ACB) -> Model method
Why the *** do we do this whole chain, when the View could change the Model?? Or no model at all! A single component
Separation of concerns. This is what the course is about. JS, JSX, Vue, React, CSS are just means
Custom event names (and Presenter ACB names) should have no graphical implication. No clicks, no buttons, no input boxes, no selects
onNumberChange(x) Number changed with buttons, input boxes, scrollbars, spinners. Don’t care. It’s the View job
Views are dumb. Only draw (render, represent) data. Data is always provided to them
Native events depend on the graphical representation. This is why Views handle them
CB, ACB works very well.
Only one case of someEvent={ACB ( ) }
Overdoing it: isDishInMenu={ACB} should be truthy or falsy! Presenter job, to calculate View props
Next statement after a promise.then(ACB) will never know the promise result!!!
Only the ACB will know the result (its only parameter)
resolvePromise(promise, promiseState); // sets data to null, does promise.then(willKnowResultACB)
return promiseState.data; // useless, always returns null!!!
Implementing Observer
JS (needed in TW3), RxJS, Other languages /platforms
Why Observer?
One-to-many broadcast notification is ubiquitous in interaction programming
Observer allows the Subject (e.g. DinnerModel) to be decoupled from its observers. Observers can change without the Model needing to care
Vue implements advanced multicast notification + update, which is why we did not need Observer thus far
Subject / Observable / Model
Observer
Presenter (MVP)
Implementing Observer - Observable
View (MVC)
Observer (persistence)
Lab (React)
Lab (React, Vue)
Subscription: model.addObserver(ACB)
Notification: when Subject changes, ACB is invoked
One-to-many notification (broadcast)
Implementing Observer (DinnerModel as Observable)
Constructor: initialize an empty array this.someArray.
addObserver(callback)
append callback the array. Arrays can contain functions
removeObserver(callback)
remove callback from the array, e.g. filter(CB) where CB checks identity: param!==callback
notifyObservers()
function invokeObserverCB(obs){ obs(); }
Call all the callbacks in the array, e.g. using forEach with the CB above
But: if an error occurs in an observer, all subsequent observers will lose the notification. Solution: �try{ /* call obs */ }catch(err){console.error(err); }
Do not write catch(e){/*do nothing*/} !!! That will lose the errors in observers!
If an observer takes too long to address the notification (cb takes long), the other observers will be delayed!
135
Call this.notifyObservers() at the end of each setter method
notifyObservers must be invoked at the end of the methods that change the model
So that the observers see the latest Model data!
Spare notifications!
We do not notify observers on doSearch, setSearchQuery, setSearchType because the search state will be moved application state (model) → component state of Search presenter
Callback checklist addObserver(ACB)
How to pass the callback? addObserver(ACB) Other params needed? No
What does addObserver(ACB) return? sometimes a CB that removes the observer! (Idiom 2)
When does addObserver(ACB) return? Immediately. Just makes a plan, that when something changes in the Subject (e.g. model), the obseverACB will be invoked. But (Idiom 1) it may call the observerACB once (synchronously!)
Who calls the callback, and sends parameters? The Subject/Observable (model), on notifyObservers()
Role (and usual name?) of callback parameters When available, name is payload
What is the callback expected to return? nothing
When is the callback called? When there are changes in the Subject = Observable = Model
Is the callback called once? Or repeatedly? Repeatedly
What is the role of the callback? Update based on the latest data from the Subject = Observable = Model
Example callback names myObserverACB
Pass a “payload” to observers
A payload is like an event fired by the Model (Observable, or Subject).
Implementation is very simple, add a parameter: notifyObservers(payload) and just pass it to the observers when calling them (in invokeObserverCB)
Example: setNumberOfGuests could call �this.notifyObservers({guests: n, oldGuests:m})
Observers can ignore the payload parameter (similar to a click event handler, it rarely needs to use the click Event object)
Persistor observer (firebase) has a use case for the payload (later)
Observer pattern implementations
JavaScript Proxy
Java
RxJS (see also RxPython, RxJava…)
const observable=new Rx.BehaviorSubject(model);
const mySubscription= observable.subscribe(console.log); // like addObserver
model.setNumberOfGuests(5);
observable.next(payload); // like notifyObservers(), will log the payload on console.log
mySubscription.unubscribe() // like removeObserver
139
Component State
Comparison to Application State (model)
Application State vs Component State
Application state (model): all components (presenters) rely on a shared state, at application level. Easy to communicate
Think of component state as a mini-model owned by the component. Each copy of the component has the state.
caretPosition
selection
horizontal scroll
text
When programming such a reusable component, we want its abstract data (state) to be associated with the component
Each copy of the component will have its own component state (text, caretPosition, …)
“Set search text”
“Set search dish type”
“Search now!”
SearchFormView
“Search result chosen”
SearchResultsView
Search
searchResults
dishTypeOptions
DinnerModel (application state)
searchResultsPromiseState
searchParams
setSearchQuery(txt)
setSearchType(type)
onSearch(params)
We use this structure in TW2 because we have an application state setup which ensures easy update of Presenter-Views and we wanted to focus on other things (fetch, promise state, promise result rendering, etc)
But let us observe that the searchParams are only used by Search. Therefore searchParams do not need to e in application state (shared between presenters)
setCurrentDish(id)
Application State only (TW2)
“Set search text”
“Set search dish type”
“Search now!”
SearchFormView
“Search result chosen”
SearchResultsView
searchResults
dishTypeOptions
We will also keep searchResults in component state because they are only used by Search presenter.
Advantage: now our search UI is less dependent on Application state. More portable. Can be shared with other programmers who work on other applications.
Disadvantage: promise state management becomes a bit more framework dependent (though we can still use resolvePromise and promiseNoData). We may also need more framework features: component lifecycle and watchers (see coming slides)
We can also move the currentDishPromiseState to Details presenter component state because only Details needs it.
DinnerModel (application state)
setCurrentDish(id)
Search
searchText, searchQuery, searchResultsPromiseState
Application State combined with Component State, including async component state
Component state principle
Component
stateMember1
stateMember2
return <div><SubComponent prop={stateMember1}/> {stateMember2} </div>
stateMember1
stateMember2
stateMember1
stateMember2
Initial render:
Update:
Complete render code runs every time, not just parts that depend on stateMember! Sub-optimal?
Reconciliation: Render in memory, then compare to the browser DOM
If any stateMember is set to its present value, there is no update! (bail-out)
Setting a stateMember to new Object() or simply {} is guaranteed to update because that produces a new value every time (“force re-render”). But see Vue selective update
React: Yes and Yes. Full update (you can use React.memo(SubComponent) to only re-render the SubComponent when its props have changed)
Vue: Yes but only if they use stateMember in the render code. Selective update
Vue scenario:
Component render: <div><SubComponent prop={stateMember1}/> {stateMember2} </div>
2. If stateMember is an object, and stateMember.property changes, will there be re-render (update)?
React: No. Change the whole stateMember to a new object!
Shallow state check
Vue: Yes, but only components that use stateMember.property in their render code! Components that only use other stateMember properties will not update.
Deep state check
Application state as Vue top-level component state (TW1, TW2)
“Deep state check”
rootModel
“Selective update”�Which components should update?
React Top-Level State: why do I get an E?
Top-Level State means that the main application data is component state in the root app.
Problem: due to React full update, whenever a bit of application data changes, the whole app is re-rendered, with all sub-components, whether or not they depend on that bit of state. Useless update, bad performance for a reasonably large app. You could partially address that with React.memo
But the most important aspect is that the top-level component mixes the Model, View and Presenter concerns! (in the lab, it’s just a View that arranges the UI graphically).
Solutions:
App
numberOfGuests
dishes
currentDish
So is Vue the best? Deep state check, selective update
Not necessarily. Why?
Consider why standard JS array or object cloning provide only shallow clones
Consider a large application, with a deep object as top-level state
React performance is more predictable.
With Vue power (deep state check, selective update), comes responsibility
You have to be aware of the specifics of the component state that you are using
Component Lifecycle
Lets you register ACBs that run
What does this have to do with component state? Use cases:
Resolving a promise in “fixed” component state
promiseState never changes, only its content. Even fixed component state is useful because it ensures that each copy of the component will have a copy of this state
In (custom) event listener or component lifecycle:
resolvePromise(p, promiseState)
Component
promiseState
promise
data
error
forceRerender (React only)
Derived component state (side effect)
Frameworks are good at giving us the current value of the state, but not the previous value. So special functionality is needed for acting on changes component state, props etc.
Sometimes a change in a component state member, or in a prop (including application state) requires further changes in component state
The original change usually happens outside the control of the component.
The Search promise resolution does not need derived state because promise changes within the control of Search (in startup lifecycle, or from “Search now” custom event).
Derived state implies registering 1-2 ACBs
ACB called with the expression changes
optionally, ACB that gets called when the change is not actual any longer (to allow for cancelling the effect).
Details
currentDish React: copy of
→ promiseState
props.model.currentDish
Render
Functional: SomeComponent(props)
Object: render()
SomeComponent
Expression E in component state or props has changed!
exprChangedACB()
<SomeComponent prop=val/> or
createElement(SomeComponent, props)
(0) first render
(1.1), (1.2), (1.3) … Component State changed. Update!
(1) A component of this type was created!
(2) Component was taken down
wasCreatedACB()
isTakenDownACB()
Lifecycle
For each framework:
0: Styles of defining stateful components
a) How to read their value during rendering?
b) How to change their value in event handlers, lifecycle or derived state ACBs?
c) How to force re-render?
React example: how to add Observers and copy state from the Model to component state
Vue: 0. Stateful component styles
APIs
Rendering
Vue 1: How to define component state members?
import {reactive} from "vue";
const Motd={ // object literal
props: ["initialMsg"],
setup(props){
const msg= reactive({ message: props.initialMsg });
return function renderACB(){
return (
<div>
<input onInput={ handleInputACB } />
<div>message of the day is: { msg.message } </div>
</div>);
};
function handleInputACB(e){ msg.message= e.target.value; }
},
}; // <Motd initialMsg="blabla" />
reactive must handle an object. For primitives (number, string), you can only use ref.�y=ref(x) is the same as y=reactive({value:x}). So then you need to use y.value
Application state as Vue top-level component state (TW1, TW2)
import {reactive, render} from "vue";
const rootModel= reactive(new DinnerModel()); // can use reactive outside any function
function VueRoot(){ return <App model={rootModel} />;} // functional component
render(<VueRoot />, document.getElementById('root'));
“Deep state check”
rootModel
“Selective update”�Which components should update?
Converting from TW1+TW2 component to Composition API
//TW1, TW2
export default function MyPresenter(props){ return <Some>{props.model.x}</Some>;}
//TW3 Search, Details, VueRoot
export default{
name: "MyPresenter", // useful for Vue stacktraces
props:["model"],
setup(props){
// here you can add component state, lifecycle, side effects!
return function renderACB(props){ return <Some>{props.model.x}</Some>;};
},
};
Vue: 2. How to register component lifecycle ACBs?
import {onMounted, onUnmounted} from "vue";
const LifeAndDeath={
setup(){
function lifeACB(){
console.log("E.g. do first search, put results in component state!");
}
function ripACB(){console.log("perform cleanup");}
onMounted(lifeACB);
onUnmounted(ripACB);
return function renderACB(){ return <span>I’m alive!</span>; };
},
};
setup() can also be used to detect when the component is “born”.
Besides onMounted() and onUnmounted(), Vue supports other lifecycle hooks
Vue complete lifecycle
Vue: 3. Derived component state (side effect)
import {watchEffect} from "vue"; // watchEffect does not check deep, see watch for advanced
const MyDetails={
props: ["model"],
setup(){
function hasChangedACB(onCleanup){
console.log("current dish changes to: ", props.model.currentDish);
// good place to resolve getDishDetails promise in component state
// you can also cancel the effect / promise, not needed in the lab:
onCleanup(function cleanupACB(){ console.log("it'll change again!");});
}
watchEffect(hasChangedACB);
return function renderACB(){ return props.model.currentDish; };
},
};
Vue detects automatically that the effect depends on props.model.currentDish!
Vue: 4. Resolve a promise in component state
import {reactive} from "vue";
import resolvePromise from ..
const MyPresenter={
setup(){
const promiseState= reactive({});
function resolve(promise){
resolvePromise(promise, promiseState);
}
// TODO event handler, lifecycle or side effect that calls resolve() with e.g. getDishDetails(someId)
return function renderACB(){
return promiseNoData(promiseState) ||
JSON.stringify(promiseState.data);
};
},
};
Vue: 5. Reuse code: custom hook
import {reactive} from "vue";
import resolvePromise from ..
function usePromise(){
const promiseState= reactive({});
function resolve(promise){
resolvePromise(promise, promiseState);
}
return {promiseState:promiseState, resolve:resolve};
}
//—------------------------
import usePromise from ..
const MyPresenter={
setup(){
const usePro = usePromise();
// TODO event handler, lifecycle or side effect that calls usePro.resolve() with e.g. getDishDetails(someId)
return function renderACB(){ /* as before, using usePro.promiseState */ };
},
};
Vue: 5. Reuse code: custom hook (with obj. destructuring)
import {reactive} from "vue";
import resolvePromise from ..
function usePromise(){
const promiseState= reactive({});
function resolve(promise){
resolvePromise(promise, promiseState);
}
return {promiseState, resolve}; // simplified JS object creation
}
//—------------------------
import usePromise from ..
const MySearch={
setup(){
const {promiseState, resolve} = usePromise(); // JS object destructuring
// TODO event handler, lifecycle or side effect that calls resolve() with e.g. getDishDetails(someId)
return function renderACB(){ /* as before, using promiseState */ };
},
};
Vue 1c) How to force re-render? (rarely used)
import {reactive} from "vue";
const DateAndTime={
setup(){
const state= reactive({ forceReRender: null });
function reRenderACB(){ state.forceReRender= new Object(); }
return function renderACB(){
state.forceReRender; // use state to "fool" selective update!
return (
<div>
<button onClick={ reRenderACB } >Set time!</button>
<div>time of the last click is: { new Date().toString() } </div>
</div>);
};
},
};
For each framework:
0: Styles of defining stateful components
a) How to read their value during rendering?
b) How to change their value in event handlers, lifecycle or derived state ACBs?
c) How to force re-render?
React example: how to add Observers and copy state from the Model to component state
React: 0. Stateful component styles
Old Class components. Classes that extend React.Component, with a render() method, can call this.setState({stateMember1:value1, stateMember2:value2}) and access this.state.stateMember1 etc
“Functional” stateful components. Programmers love functional components. Use hooks
React: 1. How to define component state members?
import {useState} from "react";
function Motd(props){ // Message of the Day
const arr = useState(props.initialMsg); // state hook
const message= arr[0];
const setMsg=arr[1];
return <div>
<input onInput={ handleInputACB } />
<div>message of the day is: { message } </div> {/* read */}
</div>;
function handleInputACB(e){ setMsg(e.target.value); } // change
}
Use: <Motd initialMsg="some initial text"/>
React: 1. How to define component state members? (with array destructuring, the most frequent use!)
import {useState} from "react";
function Motd(props){ // Message of the Day
const [ message, setMsg ]= useState(props.initialMsg); // state hook
return <div>
<input onInput={ handleInputACB } />
<div>message of the day is: { message } </div> {/* read */}
</div>;
function handleInputACB(e){ setMsg(e.target.value); } // change
}
Array destructuring, A JavaScript (not React) feature. If you don’t want to use it, see previous slide. However, many tutorial examples use it.
React: 2. How to register component lifecycle ACBs?
import {useState, useEffect} from "react";
function LifeAndDeath(props){
function lifeACB(){
console.log("E.g. do first search, put results in component state!");
return function ripACB(){
console.log("perform cleanup");
};
}
useEffect(lifeACB, []); // effect hook, must send empty array!
return <span>I’m alive!</span>;
}
React 2. Lifecycle: observer
import {useState, useEffect} from "react";
function MySummary(props){
function lifeACB(){ // the grey background can become a custom hook
props.model.addObserver(observerACB);
return function ripACB(){
props.model.removeObserver(observerACB);
};
}
useEffect(lifeACB, []);
const [guests, copyGuests]= useState(props.model.numberOfGuests);
function observerACB(){ copyGuests(props.model.numberOfGuests); }
return <span>{props.model.numberOfGuests}</span>;
} // we could use {guests} but with props.model… code is identical to TW1 !
// never mix guests with props.model.numberOfGuests in rendering!
Array destructuring, A JavaScript (not React) feature
React 5. custom hook
import {useState, useEffect} from "react";
function useModelProp(model, propertyName){
function lifeACB(){
// TODO add observer to model
return function ripACB(){
// TODO remove observer from model
};
}
useEffect(lifeACB, []);
const [value, copy]= useState(props.model[propertyName]);
function observerACB(){ copy(props.model[propertyName]); }
return value;
}
//—--- another file, import the above!
function MySummary(props){
useModelProp(props.model, "numberOfGuests"); // we ignore value
return <span>{props.model.numberOfGuests}</span>; // use model as source of truth
}
Array destructuring, A JavaScript (not React) feature
React: selective update with observers
If the Model notifies observers for a change in anything else than guests (e.g. currentDish), model.numberOfGuests does not change.
Therefore copyGuests(props.model.numberOfGuests) will not result in component update because it will set the existing value in component state, so React will bail-out the update
This is basically manual implementation of selective update.
For each Observer Presenter in the lab, you will need to consider which Model properties your Presenter depends on, and copy only these properties from Application state (model) to component state. Unit tests will check for this.
react-redux implements a similar mechanism, but automatically. Similar for Recoil.
React 1-5: rules of hooks
if(condition) useSomeHook(); else useAnotherHook() will not work!
Hooks are not real functions. You cannot call them just anywhere in the code or in different order. All hooks used by a component must always be called.
Invoke hooks (standard or custom)
Internally hooks are implemented with a queue. The next hook call simply returns the next result in the queue.
Hook names starting with use is just a convention, to show other programmers that they must obey the rules of hooks.
Custom hooks help to reuse code but also to ensure that the “rules of hooks” are respected.
React 3. Derived component state (side effect)
import {useState, useEffect} from "react";
function MyDetails(props){
const [currentDish, copyCurrent]= useState(props.model.currentDish)
// TODO: add an observer that keeps currentDish in sync with the Model
// OR use an observer custom hook to get currentDish as a copy of the model value
useEffect(hasChangedACB, [currentDish]); // [props.model.currentDish] should also work
const [promiseState, ]= useState({}); // see resolving promises next
function hasChangedACB(){
console.log("current dish has changed, time to resolve getDishDetails in promiseState!");� // note: currentDish may be falsy, in that case calling getDishDetails makes no sense
return function changedAgainACB(){// not needed in lab if using resolvePromise
console.log("current dish changed again, maybe cancel the promise?");
}
}
return <span>{props.model.currentDish} {JSON.stringify(promiseState)}</span>;
}
React: 1c) How to force re-render? �(needed for resolvePromise)
import {useState} from "react";
function DateAndTime(props){
const [, forceReRender ]= useState();
// ^^^^ ignore first array member, we don’t care about the value, React will check it
function reRenderACB(){ forceReRender(new Object()); }
return <div>
<button onClick={ reRenderACB } >Set time</button>
<div>time of the last click is: { new Date().toString() } </div>
</div>;
}
If you want to reuse this code, make a custom hook that returns reRenderACB
Ignore first element
React 4: update UI when using resolvePromise() in component state OR application state
Scenario: we know that a promise has changed and will be resolved with resolvePromise (in application state or component state). We know from:
Problem: React will not notice the change in the promise state due to shallow state check.
Solution: force re-render when promise changes and when it is resolved/rejected
function updateOnPromise(promise, reRender){
// TODO promise may be falsy. In that case the following is not needed.
promise.then(reRender).catch(reRender);
reRender(); // because the promise has changed. Will display the loading image.
}
Small downside: useless re-render in case of race condition (because the promise state will not change)
Example: use with application state. We can find out from an observer that search promise has changed:
updateOnPromise(props.model.searchResultsPromiseState.promise, reRenderACB)
Next slides: use with the promise state in component state
Several subscribers to a Promise
promise
resolvePromise
React updateOnPromise
then(ACB)
then(ACB)
When the promise resolves, both ACBs will be invoked
Therefore promises do broadcast notification, like in the Observer pattern.
Native events are also broadcast but in JSX we typically add only one listener.
Unlike Observables and native event targets, promises notify only once.
React 4. Resolve a promise in component state
(framework-independent race condition check using resolvePromise)
import {useState} from "react";
import resolvePromise from "..."
function MyPresenter(props){
const [promiseState, ]= useState({}); // fixed state, no setter
// TODO get a force re-render reRenderACB as in previous slides!
function resolve(promise){
resolvePromise(promise, promiseState); // deals with race conditions
// TODO: use updateOnPromise, pass reRenderACB
}
// TODO event handler, lifecycle or side effect that calls resolve() with e.g. getDishDetails(someId)
return promiseNoData(promiseState) || JSON.stringify(promiseState.data);
}
Ignore second element
React 4. Resolve a promise in component state
(framework-independent race condition check using resolvePromise and Proxy)
import {useState} from "react";
import resolvePromise from "..."
function MyPresenter(props){
// TODO get a force reRender() as in previous slides!
// Proxy will re-render whenever the promiseState changes.
const [promiseState, ]= useState(new Proxy(/* empty promise state as before: */ {},
{ // "proxy handler":
set(target, property, value){//a change is requested in the promise state ("target")
target[property]= value; reRender(new Object()); return true;
}
});
);
// now we can call resolvePromise as usual, no need for updateOnPromise
function resolve(promise){ resolvePromise(promise, promiseState); }
// TODO event handler, lifecycle or side effect that calls resolve() with e.g. getDishDetails(someId)
//.. render is same as before
}
JS Proxy is used by Vue to automatically detect when an object changes. We use it here to detect promise state changes and trigger re-render. Alternative to updateOnPromise
React 4. Resolve a promise in component state
(framework-specific promise resolve, uses derived state= side effect)
function MyPresenter(props){
const [promise, setPromise]= useState();
const [data, setData]= useState();
const [error, setError]= useState();
useEffect(promiseEffectACB, [promise]); // side effect when promise changes!
function promiseEffectACB(){
let cancelled;
function cancelledACB(){ cancelled= true; } // side effect cancelled → race condition!
setData(null); setError(null); // similar to resolvePromise
// TODO: promise.then(resolvedACB).catch(rejectedACB) , if promise is truthy!
// TODO define resolvedACB to call setData, rejectedACB calls setError
// OBS! to avoid race condition skip calling setData, setError if side effect was cancelled
return cancelledACB;
}
function resolve(prms){setPromise(prms);} // will trigger effect above!
// TODO event handler, lifecycle or side effect that calls resolve() with e.g. getDishDetails(someId)
return promiseNoData({promise, data, error}) || JSON.stringify(data);
} // ^^^object literal same as {promise:promise, data:data ,.. }
Vue templates
Vue: 0. Stateful component styles
APIs
Rendering
JSX rendering
import {reactive} from "vue";
const Motd={ // object literal
props: ["initialMsg"],
setup(props){
const msg= reactive({ message: props.initialMsg });
function handleInputACB(e){ msg.message= e.target.value; }
return function renderACB(){
return (
<div>
<input onInput={ handleInputACB } />
<div>message of the day is: { msg.message } </div>
</div>);
};
},
};
Template rendering
import {reactive} from "vue/dist/vue.esm-bundler";
const Motd={
props: ["initialMsg"],
setup(props){
const msg= reactive({ message: props.initialMsg });
function inputACB(e){ msg.message= e.target.value; }
return {msg: msg, inputACB:inputACB } ; // or just { msg, inputACB }
} ,
template: // JS `multi-line string`
`<div>
<input @input="inputACB" />
<div>message of the day is: {{ msg.message }} </div>
</div>` ,
};
Templates need a different vue import, in order for the Vue template compiler to run.
Single-file component (.vue extension)
<script setup>
import {reactive} from "vue";
const props= defineProps(["initialMsg"]); // just a Vue compiler hint
const msg= reactive({ message: props.initialMsg });
function inputACB(e){ msg.message= e.target.value; }
// no return! The above code is placed outside any function!
</script>
<template>
<div>
<input @input="inputACB" />
<div>message of the day is: {{ msg.message }} </div>
</div>
</template>
import Motd from "Motd.vue"
Vue template language
//searchFormView.vue
<script setup>
const props= defineProps(["dishTypeOptions", "searchCustomEvent",
"textCustomEvent", "typeCustomEvent"]);
function searchACB() { props.searchCustomEvent(); }
function changeTextACB(evt){props.textCustomEvent(evt.target.value);}
function changeTypeACB(evt){props.typeCustomEvent(evt.target.value);}
</script>
<template>
<div>
<input v-on:input="changeTextACB" />
<select v-on:input="changeTypeACB">
<option>Choose:</option>
<option v-for="opt in props.dishTypeOptions"
v-bind:key="opt" v-bind:value="opt">
{{ opt }}
</option>
</select>
<button @click="searchACB">Search!</button>
</div>
</template>
<style>/* your CSS here! */</style>
// presenter, written in normal JSX
// can be written as a template .vue file as well!
import SearchFormView from "searchFormView.vue" // .vue file below
..
return <SearchFormView dishTypeOptions={["starter", "main course", ..]}
searchCustomEvent={someACB}
textCustomEvent={anotherACB}
typeCustomEvent={someOtherACB} />
Support for custom events handling in templates
If the presenter is written as template, custom events should be emitted by the View
(note the special capitalization!)
A presenter written as template (or SFC) can send a handler only when the custom event name starts with on (or is emitted): �v-on:customEventName = "ACB" �or @customEventName= "ACB"
But you can always write the presenter in JSX (recommended) and use onCustomEvent={ACB}
//searchFormView.vue
<script setup>
const props= defineProps(["dishTypeOptions"]);
const emit= defineEmits(["searchCustomEvt","changeTextCustom","changeTypeCustom"]);
function searchACB() { emit("searchCustomEvt"); }
function changeTextACB(evt){emit("changeTextCustom", evt.target.value);}
function changeTypeACB(evt){emit("changeTypeCustom", evt.target.value);}
</script>
<template>
... as before ...
</template>
<style>/* your CSS here! */</style>
// presenter, written in normal JSX
// can be written as a template .vue file as well!
import SearchFormView from "searchFormView.vue" // .vue file below
..
return <SearchFormView dishTypeOptions={["starter", "main course", ..]}
onSearchCustomEvt={someACB}
onChangeTextCustom={anotherACB}
onChangeTypeCustom={someOtherACB} />
// same presenter, written as template (or SFC)
import SearchFormView from "searchFormView.vue" // .vue file below
..
template:`<SearchFormView :dishTypeOptions="['starter', 'main course', ..]"
@searchCustomEvt="someACB"
@changeTextCustom="anotherACB"
@changeTypeCustom="someOtherACB" />`
A few Component State remarks
React controlled components. onChange== onInput
The MOTD box is empty initially. Let us set the box content to the initial state: value={message}
function handleInputACB(e){setMsg(e.target.value);}
<input value={message} onInput={ handleInputACB } />
By setting value the INPUT becomes a React controlled component:
In Vue onInput and onChange behave as per the DOM specification
You can set the initial INPUT value in Vue as well but it will not control the component as closely as React.
<input value={this.message} onInput= { someACB.bind(this) } /> fires at every key
<input value={this.message} onChange={ someACB.bind(this) } /> fires on Enter, Tab
Don’t change component state during render! Change only in ACBs!
Component state should always be changed from an ACB! Otherwise it’ll trigger another render!
const [X, setX]= React.useState();�return <SomeView someEvent={ setX(someParam) } /> ; // wrong!!
function someACB(){ setX(someValue) ; } �return <SomeView someEvent={ someACB } /> ;
data(){ return { X: null} ; }�render(){� return <SomeView someEvent={ this.X= someParam } />
function someACB(){ this.X= someValue ; }� return <SomeView someEvent={ someACB.bind(this) } /> ;
}
When thick arrow functions were allowed in the lab it was easy to write setX() instead of ()=>setX(). Now callbacks are more explicit, hopefully it will not happen so often
The framework typically puts out a big warning when you change state during render.
Setting component state is not what it may seem
React state change is asynchronous�const [X, setX]=React.useState(A);�function someACB(){� setX(B);
console.log(X == B);
// false! It will be true only at the next render!�}
Vue wraps sub-objects with a Proxy to be able to perform deep state check�data(){ return { obj: null }; },�function someACB(){
this.obj= someObject; // e.g. {property:"value", number:42,}� console.log(this.obj == someObject);
// false!!! this.obj is a Proxy of someObject!
// someObject.property="new value" is NOT going to update UI!!
// this.obj.property= "new value" is going to update UI AND change someObject!!!
}
disabled
onMouseEnter, onMouseLeave, onMouseDown,onMouseUp
onFocus, onBlur, onKeyDown, onKeyUp
ButtonView
pressed
focus
ButtonPresenter
ButtonModel
mouseIn
mousePressed
focus
keyFocus
keyPressed
MyButton (react, vue)
onMouseEnter, onMouseLeave, onMouseDown,onMouseUp
onFocus, onBlur, onKeyDown, onKeyUp
notify!
disabled
onClick
setMousePressed
isMouseClick
Some view using the button
setKeyPressed
isKeyClick
span
style
document onMouseUp
MVP Button source code, 12 min clip: https://play.kth.se/media/MVP++Button/0_5nslwh0f
In the course: component state in Presenter, not in View. Keep the Views “dumb”!
Using component state and lifecycle for subscriptions (b, c)
Subscribing to a value (from Model or window.location.hash or …)
(mini)model value —> component state property
The model value is known as a “source of truth”
ReactRoot
DinnerModel fixed component state => no update!
App
model
model
model
Search
promiseState
model
Sidebar
guests
dishes
Summary
guests
dishes
Details
guests
dishes
currentDish
model
React state check is shallow. Any changes of state object content is ignored. The whole object (or string, number) has to change. Changing the box vs changing the content of the box.�The whole component updates, not just relevant parts.
notify! �Observer
react-redux
React-redux implements an observer-like mechanism for the Redux store
Callback checklist React.useEffect(ACB, [expr1, e2])
How to pass the callback? React.useEffect(ACB, [expr1, expr2]) Other params needed? Array of expressions
What does React.useEffect(ACB, [expr1, expr2]) return? nothing
When does React.useEffect(ACB, [expr1, expr2]) return? Immediately. Just makes a plan that when the component is rendered for the first time, ACB will be invoked. Also when expr1, expr2 change (if any are specified)
Who calls the callback, and sends parameters? React
Role (and usual name?) of callback parameters none
What is the callback expected to return? an ACB to be invoked when component taken down or expr change again!
When is the callback called? At first render, and for [expr1, expr2] at every expr change
Is the callback called once? Or repeatedly? Once for [] repeatedly for [expr1, expr2] at every expr change
What is the role of the callback? Perform subscriptions or other one-time operations for [] react to change for [e1, e2]
Example callback names wasCreatedACB for [] hasChangedACB for [e1, e2]
Train on troubleshooting. You will need it in the project!
Get familiar with
debugger
breakpoint
Step Over
Step Into
Step Out
Scopes: local, closure
Call Stack
Elements
Network
Stacktraces!
TypeError: Cannot read properties of undefined (reading 'guests')
at someACB (test.js:845:20)� at complicatedStuffThatYouDidNotWrite (test.js:4321)
The error is mot probably not in complicatedStuffThatYouDidNotWrite (or below that) because that’s most probably part of some framework, tested by lots of people.
Must be in someACB. ACBs are typicaly short, 1-2 lines.
It cannot be in a function nested in someACB. If it were, the function name would be in the stacktrace!
Clicking on line 845 in test.js will bring you to the precise line of code in the ACB, in the original source file.
Cannot read properties of undefined (reading 'guests')
means that there is something like obj.guests and obj is undefined. There can’t be many such expressions can be in the ACB. (can be x.y.z.guests, func().guests, etc)
What to do? if(obj) /* use obj.guests */ else /* treat the special case */
obj? obj.guests : 2
Navigation: routers
Use case
We want to show only one of Search, Summary, Details
Navigation:
Initially Search (aka default route)
When clicking on a dish in Search, Details should be shown. There should be a way to cancel / back to Search
If the dish is added to the menu in Details, we should go back to Search
There should be a way to go to Summary from Search, and back
When a dish is clicked in Sidebar, Details should be shown
Router
A mapping between routes and Components
/ mapped to <Search model={someModel} />
/search mapped to <Search model={someModel} />
/summary mapped to <Summary model={someModel} />
and so on
Routers are ordinary components, with some documentation. Many implementations for each framework (also one quasi-official).
You should normally be able to just use them in your project.
Problem: lots of features. Teacher role: navigate those a bit
Another teacher role: implementation principles, support in the browser
Router implementation techniques
There is no magic in programming routers. Render:
someCondition?<Search/>:<Summary/>
This technique unmounts (destroys) the component when it is hidden. Component state is lost!
Another technique, not used by contemporary routers, is to simply hide the component
CSS: .hidden{display:none;}
JSX:
<div class={someCondition?"hidden":""}<Search/></div>
<div class={someOtherCondition?"hidden":""}<Summary/></div>
This technique keeps the component state (including e.g. text written by user in textboxes, i.e. browser state). However, it has drawbacks regarding to resource use because everything is always in the browser DOM tree.
Routes in React
import { createHashRouter, RouterProvider} from "react-router-dom";
const myModel= new DinnerModel();
const routes= [
{
path: "/",
element: <Search model={myModel} />,
},
{
path: "/search",
element: <Search model={myModel} />,
},
{
path: "/details",
element: <Details model={myModel} />
},
..
];
Render code: // use previous knowledge on how to make a component, and how to render from it.
<div><Sidebar model={myModel}/></div> { /* fixed content, not affected by router */}
<div> <RouterProvider router={createHashRouter(routes)} /> </div>
Routes in Vue
import { createRouter, createWebHashHistory, RouterView} from "vue-router";
import { reactive, KeepAlive } from "vue";
const myModel= reactive(new DinnerModel());
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
component: <Search model={myModel} />,
},
{
path: "/search",
component: <Search model={myModel} />,
},
{
path: "/details",
component: <Details model={myModel} />
},
..
];
Render code: // use previous knowledge on how to make a component, and how to render from it.
<div><Sidebar model={myModel}/></div> { /* fixed content, not affected by router */}
<div> <RouterView/> </div>
Triggering navigation
Triggering hash navigation, framework independent (use in lab views!)
window.location.hash= "/details"
<button onClick={function backACB(){ window.location.hash= "/TODO"}}
>Back to Search</button>
Vue: router.push("/details")
<RouterLink to="/details">Go to details</RouterLink>
React:
import { useNavigate } from "react-router-dom";
const navigate= useNavigate();
navigate("/details")
<Link to="/details">Go to details</Link>
Component State is lost after router navigation!
This means that the search results, search text and type will be lost when coming back to search. This is not acceptable for user experience! Short-term memory…
Solutions
<RouterView>{� function routeChangeACB(x){ return <KeepAlive>{x.Component}</KeepAlive>;}
}</RouterView>
The above is an example of a callback as component child. It is a useful JSX pattern (works also in React!).
This is all you need for the lab
From here on, more information about routers and their browser support
And information useful for the project (dynamic routes)
Hash router Vs browser router
The hash router (used in the lab) uses the index.html#/details hash at the end of the URL. Originally used to navigate with anchors in large documents (e.g. wikipedia).
<a href="#/details">scroll down to details!</a>
Advantage: the browser will never send a server request when the hash changes. So we will always stay in the same page.�Perfect for single-page-applications like the ones we program in the course.
Disadvantages with Server Side Rendering (SSR).
The browser router skips the hash, but URLs shared by e.g. email will not work unless the server is configured. index.html/details ← no more hash!!!
Vue browser router: history: createWebHistory("/vue/index.html"),
React browser router: router={createBrowserRouter(routes, {basename:"/react/index.html"})}
With the browser router you can often indicate a base path, in case it is not the server root (/)
Framework independent navigation for the browser router:
window.history.pushState("", "", "/react/index2.html/details");
dispatchEvent(new PopStateEvent('popstate', {}))
Making your own router. Routers keep state!
In order to trigger update, a router needs to copy the browser hash or the history path into component state. To observe these, the router needs to add (in lifecycle) event listeners, either
window.addEventListener("hashchange", someACB)
window.addEventListener("popstate", someACB)
The ACB can copy window.location.hash or window.history.state.current into component state.
The two are like mini-application state since they are visible from all components.
Vue subscription to location.hash mini-model
// subscribe to browser-wide event (location.hash as "mini-model")�const HashSubscriber={
setup(props) {
const state= reactive({hashState: window.location.hash}); copy the value in component state
function hashListenerACB(e){ state.hashState= window.location.hash; }
// when notified, update state with current value
onMounted(function createdACB(){ // 1: The component has been created
window.addEventListener("hashchange", hashListenerACB);
});
onUnmounted(function unmountedACB(){ // 2: The component is being taken down
window.removeEventListener("hashchange", hashListenerACB);
});
return renderACB(){
return <span>{state.hashState}</span>; // for now
},
};
In Vue we must mention a state property to ensure re-render. Will not work: <span>{window.location.hash}</span>
React subscription to location.hash mini-model
function HashSubscriber(props){
const [route, setHash]= React.useState(window.location.hash); � // copy the value in component state
function hashListenerACB(){ setHash(window.location.hash); }
// when notified, update state with current value
function componentWasCreatedACB(){ // 1. the component has been created
window.addEventListener("hashstate", hashListenerACB); // subscribe, see previous slide
function isTakenDownACB(){ // 2. the component is being taken down
window.removeEventListener("hashstate", hashListenerACB);
}
return isTakenDownACB;
}
React.useEffect( componentWasCreatedACB, [] ); // empty array!
return <span>{route}</span>; // if route is /search render <Search>
// if route is /details render <Details>
// if route is /summary render <Summary>
}
React can use both the state property or {window.location.hash} during rendering, but we should not mix them!
Dynamic routes (route parameters)
/details/:ID
You could navigate to a certain dish. React router: useParams, Vue router: useRoute().params
We can imagine Sidebar and Search setting the dish route directly.
This would mean that we don’t need to save the current dish in the model.
This further illustrates that the browser hash or history can keep a bit of application state.
Route hierarchy (nested routes)
You can define sub-routes, and navigate relationally between them. This is done by defining a children array in a route, in both react-router and vue-router
Child routes inherit parameters from their parent routes
Firebase
Persist data in the cloud (lab)
Authentication and Data Security (project)
https://firebase.google.com/docs/database/web/read-and-write#web-version-9
https://firebase.google.com/docs/database/web/read-and-write#web-version-8
Lab has been converted to version 9
Writing, Reading and ref paths (see Firebase 9 docs)
const app= initializeApp(firebaseConfig);
const db= getDatabase(app);
const rf= ref(db, "some-object-name") // reference to object in cloud
set(rf, {
prop1: value1,
prop2: value2, ..
}); // returns Promise so set().then(..).catch(..)
Reading data: get(rf).then(gotDataACB)
function gotDataACB(firebaseData){ console.log(firebaseData.val()); }
get() will call gotDataACB even if there is no data for the path
In that case, val() will return undefined (including when firebase db is empty! Treat this case in the lab!)
Paths “navigate” object properties: "model", "model/guests", "model/currentDish"
get(ref(db, "some-object-name/prop1")).then(ACB) // will resolve to value1!
get(child(rf, "prop1")).then(ACB) // same as above, will resolve to value1!
Deleting! set(rf, null)
Writes and reads work offline if the browser session is not closed.
Only save primary data in firebase. Not derived data!
currentDish, not currentDishPromiseState.data
Dish id, not the whole dish!
Convert Model to something that can be persisted
Do not save whole dishes or other API objects! Save only their IDs!
If you save arrays, save them sorted. Otherwise the persistence may consider that the array elements have changed, when only the order changed
Firebase will not accept undefined values (hint: currentDish!). Save null instead
Two functions independent of firebase:
modelToPersistence(model) returns object
persistenceToModel(persistedData, model) returns promise
Using the two conversion functions in the lab
Initialization from cloud: When the app starts, initiate a get() promise, then use persistenceToModel
Update the cloud: Whenever the model changes (Observer) call modelToPersistence and save the result in Firebase using set()
Live update: (optional in the lab)
When the cloud data changes (onValue), perform persistenceToModel again and then (when the promise resolves) notifyObservers.
Goal: avoid “back and forth notification” between model and persistence. One solution:
Notify observers with a string payload.
The observer above can check the payload to recognize that the notification comes from the Live update,
If it does, it can bail out saving (back) to the cloud!
Get notified every time the Firebase object changes
onValue(ref(db, path), dataChangedACB);
// path: "model", "model/guests", "model/currentDish" etc
onValue() will call the callback at least once, even if there is no data for the object name
onValue() is a listener-style API, not a promise because it can fire repeatedly
similar to elem.addEventListener("click", ACB)
Remove listener for an object:
off(ref(db, "object-or-arr-name"), ACB)
off(ref(db, "object-or-arr-name")) // remove all
Writing and Reading Lists (multiple users) (docs)
set(ref(db, "some-array/"+ID), {
prop1: value1, prop2: value2, ..
}) // set to null if you want to remove the element!
// if you want IDs to be generated automatically:
const newItemRef= push(ref(db, "some-array"));
set(newItemRef, {
prop1: value1, prop2: value2, ..
});
// some-array can be "model/dishes"! We will not use this technique in the lab
get(ref(db, "some-array")).then(gotArrayACB)
onValue(ref(db, "some-array"), arrayChangedACB)
onChildAdded(ref(db, "some-array"), ACB)
onChildRemoved(ref(db, "some-array"), ACB)
For child_added and child_removed (assuming ACB parameter is called firebaseData)
If set() is used with an array (e.g. dishes, blog posts), and two users want to add to the array at the same time, one of them will “win” and only one array element will be added.
push() is designed for this use case, so both elements are added.
value and child_added are also fired at application startup! Even if no child was added recently, there will be a child_added at startup for each list element!
221
The persistor is a Model observer. But it also “observes” (on) other changes coming from the cloud
Model
data
application logic
no graphics
no interaction
Presenter
Provides View with the data it needs
Subscribes to get notified about changes in Model (Observer) and asks View to re-render
Receives custom events caused by User actions in the View, and asks Model to change
No graphics
View
Displays (renders) data provided by Presenter using the necessary UI elements (components)
Detects UI events from User and sends custom events to the Presenter
Refresh (re-render) when Presenter sends fresh data
subscribe
notify
doChange()
unsubscribe
custom
event
render(�data)
render(�data)
custom
event
render(�data)
custom
event
setNrGuests()
addToMenu()
removeFromMenu()
notify
subscribe
Presenter
Many Presenters observe the same Model
View
View
… there can be multiple Presenters connected to a Model, as well as multiple Views connected to a Presenter
callback
call
Persistor
Subscribes to Model, saves it (e.g. in coud) when it notifies.
Asks model to change based on persisted data.
notify
subscribe
value
child_added
set
setNrGuests()
addToMenu()
removeFromMenu()
on
firebaseModel observer!
Model notifies Firebase observer and
Firebase notifies Model onValue(ref(db, path), changeModelACB)
The risk with this setup is that we run into an infinite loop!
Firebase will not notify if you save in it the value it had before. But it’s very easy to e.g. save an array in a different order. This is why it’s good to use objects instead of arrays (use Firebase List management), or sort the arrays
To avoid this infinite loop:
Persistor
(observer)
notify
value
set
doChange()
Model
PokemonIdPresenter mapping
PokemonDataPresenter mapping
Redux Store State�
currentPokemon
currentPokemonDetails
currentPokemonError
Views:�
NumberEditor
PokemonView
Redux persistor
Pinia store
Vue Pinia Presenters
Vue Presenters
React Presenters
DOM Presenters
Recoil atoms and selectors
React Recoil Presenters
(Pinia persistor)
Recoil atom persistor
PokemonExplorer architecture and versions (see dom-redux version)
Firebase Authentication (docs). Project only
Setup: enable sign-in methods in the Firebase console �(under All products/Authentication)
sign-up/sign-in API differs across providers.
For Email / Password provider, read email and pass via e.g. HTML INPUT then call:
firebase.auth().createUserWithEmailAndPassword(email, pass)� .then(userWasCreatedACB).catch(..)
firebase.auth().signInWithEmailAndPassword(email, pass)� .then(userLoggedInACB).catch(..)
The parameter of userWasCreated and userLoggedIn is a “user credential”
In most Authentication methods:
firebase.auth().currentUser
firebase.auth().onAuthStateChanged(userLoggedInOrOutACB);
firebase.auth().signOut().then(signoutSuccessACB).catch(signoutErrorACB)
It is an interesting application to show whether various users are online. See offline capabilities
Firebase security rules
The ".read":"true", ".write":"true" rules that we set up initially allow all users to read and write everybody else’s (and your) data in the Firebase application.
This is OK for the lab (we cannot have better security rules in the lab because we don’t use Authentication)
Once Firebase Authentication is set up, you can protect various sections of the Realtime Database so e.g. only users who created certain data can retrieve it.
In the Firebase console, you can write more advanced security rules based on the ref (path) of the Firebase object. �This is JSON, not JavaScript, so only strings are allowed.
{
"rules": {
"users": {
"$uid": {
// Allow only authenticated content owners access to their data
".read": "auth != null && auth.uid == $uid" ,
".write": "auth != null && auth.uid == $uid"
},
"posts": {
"$uid": {
// Allow only authenticated content owners to write to their data, all authenticated can read (e.g. forum posts)
".read": "auth != null" ,
".write": "auth != null && auth.uid == $uid"
}
}
}
}
ref("users/31415") will only be accessible to the user with ID 31415
ref("posts/31415/101") will only be writable by the user with ID 31415 (the author) but will be readable by all authenticated users
Application State
Model, Context, Redux, Pinia, Recoil
(whatever approach you use in TW3, Views stay as they are)
Context
React: Context, Vue: Provide/Inject
Problem: sharing Application state between components
Current approach: Model, Observer.
Top-level state in React leads to lots of re-render, thus low performance
Top-level state in Vue requires prop drilling the application state from root to its users (in Tutorial, model prop in App and Presenters).
Derived state is an issue that needs to be dealt with at Model / Application state level.
Component hierarchy !== Application State hierarchy and dependencies
(so top-level state has inherent issues in larger applications)
React Context (avoids prop drilling)
const ModelContext = React.createContext();
<ModelContext.Provider value={myModel}>
<App />
</ModelContext.Provider>
App :
function App(){ // no need for props
return <div>
<div><SidebarPresenter /></div>
<div class="mainContent"><SummaryPresenter /></div>
</div> ;
}
Presenter :
function SidebarPresenter(){ // no need for props
const model= React.useContext(ModelContext);
const [, copyGuests]= React.useState(model.numberOfGuests);
..}
Problems:
“Context hell” , gives a fixed number of shared values:
<Context1.Provider value={v1}>
<Context2.Provider value={v2}>
<App/>
</Context2.Provider>
</Context1.Provider>�Context is not optimized for frequent updates (meant for e.g. change theme, language)
Vue provide-inject:
const MyProvider={
provide(){ return {p1:v1, p2:v2};},
render(){ return this.$slots.default(); },
..};
const ContextUser={
inject: ["p1", "p2"],
..};
<MyProvider>
<Etc/><Bla><ContextUser/></Bla>
</MyProvider>
Recoil
Experimental technology for React Application State�v. 0.11 in VT21, VT22 at v. 0.6, HT22 v. 0.7.6 (mid-October)
const guests= useRecoilState(applicationState);
no more Observer in React component state!
Derived application state built-in. Primary state -> atoms, derived state-> selectors
Async data treated like any other data. No need for resolvePromise or promiseNoData
Recoil PokemonExplorer, first step
// atoms.js
const pokemonIDState = Recoil.atom({
key: 'CurrentPokemonID',
default: 1,
});
// presenter code:
function PokemonIdPresenter {
const [pokeId, setPokeId]= Recoil.useRecoilState(pokemonIDState);
return <NumberEditor number={pokeId} setNumber={setPokeId} />;
}
// test:
<Recoil.RecoilRoot>
<PokemonIdPresenter />
</Recoil.RecoilRoot>
Now any component can use the pokemonIDState, they will all be kept in sync! Week 1 is a breeze :)
But how about the pokemon data? Derived state.
Recoil side effects (selectors) & async. Suspense
// atoms.js
const pokemonData = Recoil.selector({
key: 'CurrentPokemonData',
get: function(recoil){ return fetch("https://pokeapi.co/api/v2/pokemon/"+
recoil.get(pokemonIDState))
.then(function(r){ return r.json(); }); }
});
// presenter code
function PokemonDataPresenter(){
const [data]= Recoil.useRecoilState(pokemonData);
return <PokemonView poke={data} />;
} // or return <PokemonView poke={Recoil.useRecoilValue(pokemonData)} />;
// test:
<Recoil.RecoilRoot>
<PokemonIdPresenter />
<React.Suspense
fallback={<img src="https://www.csc.kth.se/~cristi/loading.gif"/>}>
<PokemonDataPresenter />
</React.Suspense>
</Recoil.RecoilRoot>
Recoil selectors are designed to represent derived state (remember currentDish->currentDishData)
Selectors can read atoms (get) but they can also change them (set)
Selectors must be pure functions (always return the same result for a certain value of the atoms they depend on). Recoil may cache/reuse the result!
Selectors can return Promise instead of a result! In that case, components that use such selectors must be surrounded with <React.Suspense> (remember promiseNoData). To also catch the promise errors, add a React.ErrorBoundary around the Suspense.
Derived state/selectors do not need to be asynchronous. You can have the atom dishes and the selector ingredients!
Any component from the React component tree can use any atom or selector through the same API (useRecoilState() or useRecoilValue()) without caring if it’s atom (primary state) or selector (derived state).
Therefore atoms can be easily changed into selectors (or the other way around) without changing the UI code or selectors that depend on them.
Source: Recoil video
Recoil “application logic”. Dependency graph
Problem: we want the pokemonIDState atom to always have a legal value.
Solution (there may be others, or there will be!): we turn pokemonIDState into a (writable) selector. All selectors and components that depend on pokemonIDState (including pokemonData) will still work. Throwing an exception in a selector will remove components that depend on it. So we set another atom with the error.
const pokemonIDAtom = Recoil.atom({
key: 'CurrentPokemonID',
default: 1,
});
const pokemonIDState = Recoil.selector({
key: 'CurrentPokemonIDGuard',
get: function (recoil) {return recoil.get(pokemonIDAtom);},
set: function (recoil, newValue) {
if(newValue>=1){recoil.set(pokemonIDAtom, newValue); recoil.set(pokemonIDError, null);}
else recoil.set(pokemonIDError, "bad value");
}
});
const pokemonIDError = Recoil.atom({
key:'PokemonIDError',
default: null
});
Atom and selector dependencies
Recoil persistence: atom effects (unstable API!)
const pokemonIDAtom = Recoil.atom({
key: 'CurrentPokemonID',
default: 1,
effects_UNSTABLE: [
({onSet, setSelf}) =>{
setSelf(get(ref(db, "pokemonId") // a promise!
.then(data=>data.val() || new Recoil.DefaultValue()));
onSet(newID => set(ref(db, "pokemonId"), newID) );
onValue(ref(db, "pokemonId"), data=>
/* setSelf(data.val()) as above, only if data.val() is not null */);
return ()=>off(ref(db, "pokemonId")); // callback to execute at atom cleanup
},
],
});
// Test:
<Recoil.RecoilRoot>
<React.Suspense fallback={<img src="http://www.csc.kth.se/~cristi/loading.gif"/>}>
<PokemonIdPresenter />
<React.Suspense fallback={<img src="http://www.csc.kth.se/~cristi/loading.gif"/>}>
<PokemonDataPresenter />
</React.Suspense>
</React.Suspense>
</Recoil.RecoilRoot>
The first setSelf()
TW3 in Recoil (example)
Atom and selector dependencies
Persist numberOfGuestsAtom, dishesAtom, currentDish to Firebase.
Derived State summary: component vs application state
Technology | Primary state | Derived state | UI for async data |
React component state | State property current dish | useEffect current dish data | e.g. pomiseNoData React.Suspense |
Vue component state | State property | watchEffect, watch, computed | e.g. pomiseNoData |
Model | object property searchParams | Setter that changes multiple model properties, resolvePromise, search results | e.g. pomiseNoData |
Redux | Reducer | Action creator (more dispatches) | |
Pinia | ref(...) | computed | e.g. pomiseNoData |
Recoil | Atom | Selector | React.Suspense |
ReactiveX (eg. RxJS) | Subject (observable) | pipe, merge, ... | |
Redux
React-Redux, using Redux with Vue
Redux: store, state, dispatch, actions
A standardized “model”. Multicast Observable like the lab DinnerModel.
Independent of React (separation of Application state (Redux) and Presenter-View= React/UI concerns).
CurrentState + dispatch(action) → NextState
One single mutating (changing) method: dispatch. Its parameters are actions
model.setNumberOfGuests(3) store.dispatch({type:"SET_NO_GUESTS", value:3})
model.addToMenu (dish) store.dispatch({type:"ADD_DISH", dish:dish})
model .numberOfGuests store.getState().numberOfGuests
model.addObserver(obs) store.subscribe(obs) // returns ()=>unsubscribe(obs)
Unidirectional data flow:
Simple “big” reducer
The reducer gets the old state and an action, and computes the new state
function bigReducer(state={numberOfGuests: 2, dishes:[]}, action){
if(action.type==="SET_NO_GUESTS")
return {... state, numberOfGuests: action.value}; // new state!
if(action.type==="ADD_DISH")
return return {... state, dishes: [...state.dishes, action.dish]} ;
if(action.type==="REMOVE_DISH")
return /* TODO remove the dish action.value from state.dishes */;
// we did not recognize the action so we return the old state
return state;
}
// test your reducer at the console! This is what Redux does internally
bigReducer(undefined, {type: "dummy"}) // {numberOfGuests:2, dishes:[]} ,
// this is how Redux computes the initial state
bigReducer({numberOfGuests:3, dishes:[]}, {type:"SET_NO_GUESTS", value:6}) // {numberOfGuests:6, dishes:[]}
const myStore= Redux.createStore(bigReducer);
// test: myStore.getState(); myStore.dispatch( {type:"SET_NO_GUESTS", value:6}); myStore.getState()
Redux state is immutable: a new store state object is created after each action. Memory issues.
Reducers must be pure functions (always return the same results for given parameters, and no side effects)
Application logic can be performed in reducers or in action creators (later)
Combining simpler reducers
function numberOfGuests(state=2, action){
if(action.type==="SET_NO_GUESTS")
return action.value; // new state!
return state; // state returned unchanged
}
function dishes(state=[], action){
if(action.type==="ADD_DISH")
return /* TODO action.dish appended to state*/;
if(action.type==="REMOVE_DISH")
return /* TODO expression with state and action */;
return state;
}
function bigReducer(state={}, action){ // manual combineReducers, see below
return { // object creation literal
numberOfGuests: numberOfGuests(state.numberOfGuests, action),
dishes: dishes(state.dishes, action),
};}
const myStore= Redux.createStore(bigReducer)
Redux.createStore(Redux.combineReducers({ numberOfGuests:numberOfGuests, dishes:dishes}))
Redux.createStore(Redux.combineReducers({ numberOfGuests, dishes})) // JS equivalent ^^^
SummaryPresenter
SummaryView
subscribe
Redux Store State�
�
numberOfGuests
dishes
currenDish
SidebarPresenter
SET_NO_GUESTS
notify
SidebarView
setNumber
render�(number, dishes)
subscribe
notify
REMOVE_DISH
removeDish
render�(people, ingredients)
currentDishAction()�
dishChoice
actionCreator() (explained later)
notify
subscribe
Internal in react-redux, �Vue with Redux...
With Redux, instead of method calls, we have actions between Presenter and Application State
Mapping Redux store to component props/events
// we aim for: <SidebarView guests=.. dishes=.. setNumber=.. removeDish=.. />
function mapStateToProps(state){ // props
return {
guests: state.numberOfGuests,
dishes: state.dishes,
};
}
function mapDispatchToProps(dispatch){ // custom event handlers
return {
setNumber: function nrACB(x){dispatch({type: "SET_NO_GUESTS", value:x});},
removeDish: function rdACB(x){dispatch({type: "REMOVE_DISH", /*TODO*/});},
dishChoice: function dcACB(x){dispatch(/* see action creators later */});},
};
}
const SidebarPresenter= ReactRedux.connect(mapStateToProps, mapDispatchToProps)(SidebarView);
<ReactRedux.Provider store={myStore}>
<ReactRoot />
</ReactRedux.Provider>
ReactRoot : ..<SidebarPresenter />..
Generating the Presenter by mapping the store state/dispatch to the View is very elegant architecturally. TW3 is about writing presenters, so this comes in handy. Works for Vue as well! (see later slides)
If you still want to write a Presenter by hand, ReactRedux provides some custom hooks. Normally you should not need ReactRedux custom hooks. Maybe your mappings can be improved?
connect() returns a function, which is then applied to a component (SidebarView). The function is called a higher-order component.
You can use any name for mapStateToProps, mapDispatchToProps
Redux PokemonExplorer, first step
// reducers.js
function currentPokemon(state=1, action){
if(action.type==="SET_POKEMON_ID")
return action.value;
return state;
}
const myStore=Redux.createStore(combineReducers({currentPokemon: currentPokemon}))
// same as: const myStore=Redux.createStore(combineReducers({currentPokemon}))
// mapToProps.js
// We aim for <NumberEditor number=.. setNumber=.. />
function mapStateToEditorProps(state){ return {number: state.currentPokemon}; }
function mapDispatchToEditorProps(dispatch){ return {setNumber: x=> dispatch({type:"SET_POKEMON_ID", value:x }); }
// bootstraping code, can be in HTML
const PokemonIdPresenter= ReactRedux.connect(mapStateToEditorProps, mapDispatchToEditorProps)(NumberEditor);
..
<ReactRedux.Provider store={myStore} >
<PokemonIdPresenter />
</ReactRedux.Provider>
But how about the pokemon data? Derived state (pokemon data is derived from pokemon id)
Separation of concerns!
Promise state in Redux
function currentPokemonDetails(state=null, action){
if(action.type==="SET_POKEMON_ID") return null;
if(action.type==="SET_POKEMON_DETAILS") return action.value;
return state;
}
function currentPokemonError(state=null, action){
if(action.type==="SET_POKEMON_ID") return null;
if(action.type==="SET_POKEMON_ERROR") return action.value;
return state;
}
const myStore=Redux.createStore(Redux.combineReducers({
currentPokemon,
currentPokemonDetails,
currentPokemonError}));
Redux action creators: derived state & async
// reducers.js currentPokemonDetails derived from currentPokemon
// action creator ensures derived state and application logic
function currentPokemonAction(dispatch, pokemonId){
if(pokemonId<=0) throw "Invalid pokemon id";
fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId)
.then(r=>r.json())
.then(d=> dispatch({type:"SET_POKEMON_DETAILS", value: d}))
.catch(e=> dispatch({type:"SET_POKEMON_ERROR", value: e}));
return {type: "SET_POKEMON_ID", value: pokemonId};
}
function currentPokemon(state=1, action){/*as before*/}
function currentPokemonDetails(state=null, action){
if(action.type==="SET_POKEMON_ID") return null;
if(action.type==="SET_POKEMON_DETAILS") return action.value;
return state;
}
function currentPokemonError(state=null, action){
if(action.type==="SET_POKEMON_ID") return null;
if(action.type==="SET_POKEMON_ERROR") return action.value;
return state;
}
const myStore=Redux.createStore(Redux.combineReducers({
currentPokemon,
currentPokemonDetails,
currentPokemonError}));
// mapToProps.js
function mapStateToEditorProps(state){
return { number: state.currentPokemon };
}
function mapDispatchToEditorProps(dispatch){
return { setNumber:
x=> dispatch(currentPokemonAction(dispatch, x))
};
}
// <PokemonView poke= ../>
function mapStateToPokemonProps(state){
return {
poke: state.currentPokemonDetails,
promise: state.currentPokemon,
data: state.currentPokemonDetails,
error: state.currentPokemonError,
// needed for promiseNoData below
};
}
// bootstraping
const PokemonSuspense= props=>� promiseNoData(props.promise, props.data, props.error)
|| <PokemonView {...props} />;
const PokemonDataPresenter= ReactRedux.connect(mapStateToPokemonProps)
(PokemonSuspense)
For simple cases, it’s perfectly OK to work without Redux middleware dedicated to asynchronous work (Thunk)
PokemonExplorer: derived state & avoid race conditions
Action order assumed by current code
SET_POKEMON_ID, SET_POKEMON_DETAILS, SET_POKEMON_ID, SET_POKEMON_DETAILS
Possible action order (user event before promise resolves!)
SET_POKEMON_ID, SET_POKEMON_ID, SET_POKEMON_DETAILS, SET_POKEMON_DETAILS
SET_POKEMON_ID, SET_POKEMON_ID, SET_POKEMON_DETAILS, SET_POKEMON_DETAILS
In that case, the SET_POKEMON_DETAILS corresponding to an “old” ID must be ignored:
function currentPokemonAction(dispatch, pokemonId){
if(pokemonId<=0) throw "Invalid pokemon id";
fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId)
.then(r=>r.json())
.then(d=> myStore.getState().currentPokemon === pokemonId &&
dispatch({type:"SET_POKEMON_DETAILS", value: d}))
.catch(e=> myStore.getState().currentPokemon === pokemonId &&
dispatch({type:"SET_POKEMON_ERROR", value: e}));
return {type: "SET_POKEMON_ID", value: pokemonId};
}
Problem: we are using myStore as a global, albeit for reading. This can cause issues on server-side rendering.
Redux Thunk middleware lets us access the whole state in an action creator.
Solution without accessing all Redux state (which is a problem according to Redux creator):
function currentPokemonDetails(state={}, action){
if(action.type==="SET_POKEMON_ID")
return {expectedId:action.value};
if(action.type==="SET_POKEMON_DETAILS" &&
action.value.id===state.expectedId)
return {data: action.value};
// else do not change state; ignore stale data
return state;
}
Async derived state. Thunk
Redux Thunk middleware allows you to write action creators that return a callback instead of an action, with the dispatch and state as parameters, which will be set by Thunk.
function currentPokemonAction(pokemonId) {
return function( dispatch, getState ) { // thunk callback
if(pokemonId<=0) throw "Invalid pokemon id";
dispatch({type: "SET_POKEMON_ID", value: pokemonId});
fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId) .then(r=>r.json())
.then(function gotDataACB(d){
if(getState().currentPokemon === pokemonId) // avoid race condition
dispatch({type:"SET_POKEMON_DETAILS", value: d});
})
.catch(function gotErrorACB(e){
if(getState().currentPokemon === pokemonId)
dispatch({type:"SET_POKEMON_ERROR", value: e});
})
}; // end of thunk callback
}
const myStore = Redux.createStore(/*reducers*/,
Redux.applyMiddleware(ReduxThunk.default));
myStore.dispatch(currentPokemonAction(1)); // initialize currentPokemonData!
// mapToProps.js
function mapDispatchToEditorProps(dispatch){
return {
setNumber: function nrACB(x){
dispatch(currentPokemonAction(x));
})
};
}
Alternative to Thunk: �Redux Saga
DetailsPresenter
(use promiseNoData)
DishDetailsView
render�(dish, people, isDishInMenu)
subscribe
notify
Redux Store State�
searchType
searchText
searchResultData
currentDish�
currentDishData
currentDishError�
numberOfGuests
dishes
SearchFormPresenter
SearchResultsView
searchAction()
notify
SearchFormView
onSearch
onText
onType
currentDishAction()�
dishChosen
render�(searchResults)
dishAdded
ADD_DISH
subscribe
actionCreator()
SearchResultsPresenter
(use promiseNoData)
notify
subscribe
Internal in react-redux, �Vue with Redux...
Redux pros and cons
Easy to add new properties to the application state
Easy to map store state to components props.
Addresses TopLevelState and Context issues.
Functional. Reducers are pure functions, state is immutable
Easy to provide Persistence, Caching and Undo-Redo for any Redux store.
Nice debug tools, e.g. time-travel debugging
Cons:
Persisting the Redux state
(libraries like firebase-redux)
1. Listen to the Redux store using store.subscribe()
2. Listen to the persistence storage and dispatch actions!
Simple react-redux implementation
const MyReduxContext = React.createContext();
function myConnect(mapStateToProps, mapDispatchToProps){
return function(View){
return function(){ // a presenter with no props, in reality a class/object
const store=React.useContext(MyReduxContext);
const [viewProps, setViewProps]=React.useState(); // should optimize...
React.useEffect(function createdACB(){
return store.subscribe(function observerACB(){ // like addObserver
const props= mapStateToProps? mapStateToProps(store.getState()) :{};
const events= mapDispatchToProps? mapDispatchToProps(store.dispatch) :{};
setViewProps({...props, ...events}); // JS object spread
});
}, []);
return <View {... viewProps} />; // JSX props spread
};
}
}
const PokemonIdPresenter= myConnect(mapStateToEditorProps, mapDispatchToEditorProps)(NumberEditor);
// remove the react-redux import and test:
<MyReduxContext.Provider value={myStore} >
<div>
<PokemonIdPresenter/>
<PokemonDataPresenter/>
</div>
</MyReduxContext.Provider>
Vue with Redux
const ReduxVueProvider={
props:["store"],
setup(props, context){
provide("theStore", props.store); // Vue context (provide-inject), see inject below
return function renderACB(){ return context.slots.default(); } // render all children
},
};
function reduxVueConnect(mapStateToProps, mapDispatchToProps){
return function(View){
return { // presenter
setup(){
const store= inject("theStore"),
const props=reactive({});
function observerACB(){ props.value= mapStateToProps? mapStateToProps(store.getState()) : {};}
const events= mapDispatchToProps? mapDispatchToProps(store.dispatch) : {};
let unsubscribe;
onMounted(function subACB(){ unsubscribe= store.subscribe(observerACB) ;}
onUnmounted(unsubscribe);
return function renderACB(){ return <View { ...{...props.value, ...events}} />;} //object + JSX spread
},
}; // end of presenter
};
}
const PokemonIdPresenter= reduxVueConnect(mapStateToEditorProps, mapDispatchToEditorProps)(NumberEditor);
<ReduxVueProvider store={myStore} >
<div>
<PokemonIdPresenter />
<PokemonDataPresenter />
</div>
</ReduxVueProvider>
Usability and User Experience (UX)
Concepts and discount methods
Usability (DH2620)
Effectiveness, Efficiency, Satisfaction, in a specified Context of use
Effectiveness: accuracy in relation to the user task
Efficiency: speed with which the user can perform the task
Learnability (e.g. discovering what an app does)
Sparking user Engagement
Error tolerance
This is traditional, evaluative HCI (Human-Computer Interaction). Sufficient for the course
Usability Heuristics, Heuristic evaluation
How to quickly improve usability?
Without users: heuristic evaluation . Apply Nielsen/Schneiderman heuristics
With users:
Project preparation
Architecture/code well-separated concerns.
Clean console (e.g. array rendering keys, no state change during render).
Use of 3rd party components.
A+: use Redux mappings (if using Redux) to generate Presenters
Usability/User experience/ improve usability user feels in control, user satisfaction, functionality easy to discover
clear target group, clear added value for the user, evaluation with users (formative and summative)
A+ UX prize for App idea beyond state of art, or simply well done app UX (satisfaction)
Web APIs used match use case, data persisted per user, visibility of system status when accessing API
Group cooperation every student must commit! Good work balance, good indication of role separation between group members
Presenter-View separation
Use cases
Presenter-View recap
Presenter
View
The Presenter is responsible with
Presenter and View must co-vary independently. Similar to an Observer and the Observable (Model). Change one, the other stays the same.
“Language” motivation
View: lots of HTML, CSS (JSX), declarative, native events
Presenter: mostly JavaScript, data processing, possibly complicated, procedural processing
View changes, Presenter does not!�Custom events stay the same!
Choose the dish types in the Search form to 3 radio buttons.
Represent the Search results as a SELECT
The UI elements and the events they fire are the business of the View. When sending custom events to the Presenter, the View never includes any UI/event information.
What should the View send to the Presenter?
That is, what should be the Custom event parameters?
Presenter does not know about UI details.
Presenter is high-level (abstract, above UI details)
Do not name the custom events in a UI fashion because Presenter is ignorant of the UI details
Presenter changes, View does not: change interaction semantics
Changing the semantics of interaction can be done without changing the View. This illustrates that View (rendering data, native events) and Presenter (semantics of user interaction) are two different concerns
SearchPresenter:
<SearchFormView options={["starter", "main course", "dessert"]} � onSearch={searchACB} � onText={changeTextACB} � onDishType={changeTypeACB}� />
Instead of waiting for the Search button to be pressed we can choose to trigger search:
Sometimes you will have to touch the view though
Presenter changes, View does not: changing framework(s), changing Application State store
With a well-encapsulated View, writing a Presenter in another framework is easy (see TW3)
Changing application state from simple JS object (model) to Redux or Vuex, or Recoil, will not affect the Views
Same user events, different semantics
<DrawingCanvas shapes={arrayFromModel}
onDragStart={(point, shape)=> controllers[mode].onDragStart(model, point, shape)}
onDragTo={point=>controllers[mode].onDragTo(point)}
onDragCancel={()=>controllers[mode].onDragCancel()}
/>
The user can drag and drop on a drawing canvas (implemented e.g. with HTML CANVAS). Depending on the drawing editor mode (Select, Line, Rectangle, Ellipse) selected on a Toolbar (left), the meaning (semantics) assigned by the DrawingPresenter to the events is different:
The modes in the DrawingPresenter can be programmed as separate functions/strategies, which correspond to the notion of Controller (in MVC one View can have many Controllers)
The View is completely ignorant of the modes! (again, separation of concerns)
const controllers={
select: new SelectController(),
line: new LineController(),
..
};
class SelectController{
onDragStart(model, point, shape){
this.draggedShape=shape;
this.dragStart= this.last= point;
this.md=model;
}
onDragTo(point){
this.md.move(this.draggedShape,
point.x-this.last.x,
point.y-this.last.y);
this.last=point;
}
onDragCancel(){
this.md.move(this.draggedShape,
this.dragStart.x-this.last.x,
this.dragStart.y-this.last.y);
}
}
Vue templates in the project
As you can see, templates and JSX are freely interchangeable for simple cases
Therefore you can use templates in the project provided you respect View-Presenter separation
Warning: there is a tendency to put the View in the <template> and the presenter in <script>.
Our recommendation is to write the Presenter in JSX and the View in template but any other combination (template-template, template-JSX, JSX-JSX) is acceptable
Components you reuse will probably be written in template. The previous slide (and the project coach) can show you how to connect them to your JS / JSX presenter.
Project preparation
Installation
create-react-app vite and create-vue will install everything needed (and more) for React or Vue development respectively.
These tools do what we did for the lab setup (so you don’t have to do this manually, this list is FYI)
create-react-app and vue-cli also perform a git init.
Vite
create-vue
create-vue
create-react-app with Vite
React Scaffold
/public | This folder is used to store static assets that are not processed by Webpack/Vite (eg. logos, fonts, etc) |
src/assets | This folder is used to store assets that are processed by Webpack/vite (eg. stylesheets, JavaScript modules, and images that are imported into your React components) |
src/App.jsx & src/App.css | “Jsx” file representing an initial view of the Application and the associated css |
src/main.jsx & src/index.css | Entrypoint to application & associated css for whole app |
index.html | Html entrypoint to application (do not delete) |
package.json | Stores configuration that is used to manage the dependencies and scripts of a Node.js project |
vite.config.js | Stores configuration used by the Vite build tool |
Vue Scaffold
/public | This folder is used to store static assets that are not processed by Webpack/Vite (eg. logos, fonts, etc) |
src/assets | This folder is used to store assets that are processed by Webpack/vite (eg. stylesheets, JavaScript modules, and images that are imported into your React components) |
index.html | Html entrypoint to application (do not delete) |
package.json | Stores configuration that is used to manage the dependencies and scripts of a Node.js project |
vite.config.js | Stores configuration used by the Vite build tool |
src/components/* | Includes reusable components |
src/views/* | Includes initial pages of application (HomeView, AboutView) |
src/router/index.js | Router config file |
App.vue | Initial App scaffold. Includes basic logic for routing to HomeView and AboutView and displaying some components |
main.js | Initial entrypoint to application |
***By default Vue creates “.vue” SFC, these can be converted to “.jsx” manually***
Jsx in Vue
// SomeComponent.jsx
import { defineComponent } from "vue";
export default defineComponent({
setup(props) {
return function renderACB() {
return <div>Hello World</div>;
};
},
});
Create a new repository on e.g. https://gits-15.sys.kth.se/
Create a git repository
In your Local Repository (make sure to be in the correct folder):
git init
git add -A
git commit -m "initial commit"
Then to Push to Git :
Copy your personal script by pressing here
.gitignore
You must not commit node_modules!
You must not commit config files (API, firebase)
You must not commit public other than e.g. CSS files you may have placed there (public/style.css in the lab)
npm run build will put lots of files in public. Please do not commit them.
Imports and bootstrapping
Vue: JSX is already configured via the plugins: "@vitejs/plugin-vue-jsx" (Vite).
These plugins are usually automatically configured when setting up the project (see package.json, vite.config.js)
Note: the default Vue bootstrapping file is main.js and it imports some .vue components. But you can use JSX instead, like in the lab. Then you can start importing JSX components from .jsx files, combine .jsx imports with .vue imports, etc
// main.jsx
import {render} from "vue";
render(<h1>Hello Vue JSX World</h1>, document.getElementById("app"));
npm i firebase@9.17.1
In your persistence module (lab firebaseModel.js) you should install and setup firebase
In the lab, it was done differently via teacherFirebase.js to be testable
ESLint & Prettier
ESLint & Prettier
ESLint & Prettier
Vue, Pinia, React, Redux have their own developer tools
You can install them as a browser plugin for at least Chrome
The added value may vary
Notably Redux and Pinia allow time-travel debugging
API:s
Where to find 3rd party components
Vue: https://github.com/vuejs/awesome-vue#components--libraries
React: https://github.com/enaqx/awesome-react#react-component-libraries
CSS frameworks are NOT considered 3rd party components but they allow for faster development and a nicer design
CSS Frameworks: https://github.com/troxler/awesome-css-frameworks#awesome-css-frameworks-
Routers and Custom Hook / Composable Libraries do not count as third party components for grading
Coaching
“I’m using the latest custom hook from CoolTechnology. Why don’t I get an A?”
function MyComponent(props){
const someData1= useCoolTechnologyHook(props.param1);
const [someData, error]= usePersistenceTechnologyHook(props.param2);
return <div>
<someHTML attr={someData1} /><someHTML attr={someData2} />
</div>;
}
There are at least 4 architectural mistakes in this component. Architecture grade: ~E
Technologies (including react-redux) provide custom hooks for some of their functionality, (custom hooks are “new” and cool)
But they also provide that functionality, and much more, in other forms.
Always choose the form that fits MVP better!
Technologies will often demo/exemplify their functionality quickly, in a single component. Inevitably a single component will mix architectural concerns (V, P, persistence…)
“But you teachers may have missed this supercool custom hook! Cutting edge!”
Answers: technologies come and go, architectural principles stay. They’ve been around since the 1970s
In short, MVP architecture has precedence over framework/library examples and tutorials.
“My custom hook does everything, why do I need a Presenter?”
function DoIReallyNeedThisPresenter(props){
const fatData= useMyGreatHook(props.modelOrStore);
return <MyView prop1={fatData.field1} prop2={fatData.field2} />
}
// or even <MyView data={fatData} />
The argument is for rendering UI directly, i.e. a View implementation… Similar to Vue <template> and <script>
return <div><SomeHTML>{fatData.field1} …
One can argue that the custom hook is the Presenter concern and the rest of the component is the View concern. But how about events?
For a high grade the Presenter must be a separate object/function than the View!
With the goal of providing data to the View
And changing the Model /Application State based on View-triggered events
When the Presenter becomes simple, you can consider generating it with a function (like ReactRedux.connect())
Shortcuts like above may occur in the industry but you must be aware of the architectural compromises you are making.
Think about the developer that comes after you and may expect a clean MVC /MVP separation.
Composables and Hooks
Composables (Vue) and Hooks (React)
However,
VueUse
React-Use
useHooks
Recommended Composables / Hooks
VueUse
React-use
useHooks
DH2643 Advanced Interaction Programming
Vue - Nuxt
React - Next
What they do?
Advanced Vue Concepts
Vue Single File Components (SFC) and “.Vue” File Format
// Presenter.vue
<script>
import View from "./View.vue"
export default {
// In order to access the View
// in the Template
components: {View},
data() {
return {
greeting: "Hello World!",
};
},
};
</script>
<template>
<View :greeting="greeting"/>
</template>
// Presenter shouldn’t have
// style tag
<style></style>
Warning:
// View.vue
<script>
export default {
props: ["greeting"],
};
</script>
// Include HTML here
<template>
<p class="greeting">{{ greeting }}</p>
</template>
// Include CSS here
// Remember to include scoped to isolate
// CSS to this file
<style scoped>
.greeting {
color: red;
font-weight: bold;
}
</style>
Advanced Vue - Composition API
Which to Use?
Options Api
Ideal for low to medium level complexity applications and for beginner developers.
Composition Api
Ideal for high complexity applications and for intermediate/advanced developers
Vue Composition API
<script>
import { ref } from "vue";
import View from "./View.vue"
// If not using JSX
export default {
// In order to access the View in the Template
components: {View},
// `setup` is a special hook dedicated for composition API.
setup() {
const count = ref(0);
// expose the state to the template
return {
count,
};
},
};
</script>
<template>
<View :count=”count”/>
</template>
Vue Reactive and Mutating State (DISCOURAGED)
<script>
import { reactive } from "vue";
import View from "./View.vue"
export default {
components: {View},
setup() {
// Reactive state
const state = reactive({ count: 0 });
// Function that increments the state
function increment() {
state.count++;
}
// don't forget to expose the function as well
return {
state,
increment,
};
},
};
</script>
<template>
<div>
<View
:count="count"
@increment="increment"
/>
</div>
</template>
Vue Ref and Mutating State
<script>
import { ref } from "vue";
import View from "./View.vue"
export default {
components: {View}
setup() {
// Ref state
const count = ref(0);
// Function that increments the state
function increment() {
// Ref returns a ref object
console.log(count); // {value: 0}
console.log(count.value); // 0
// Refs can only be mutated when .value is added
count.value++;
}
// don't forget to expose
return {
count,
increment,
};
},
};
</script>
<template>
<div>
<View
:count="count"
@increment="increment"
/>
</div>
</template>
Vue Computed and Filtering Data
<script>
import { ref, computed } from "vue";
export default {
setup() {
// Ref state
const students = ref([
{ name: "Susan", favFramework: "Vue" }, { name: "Samantha", favFramework: "React" },
{ name: "Rabi", favFramework: "React" }, { name: "Rebecca", favFramework: "Vue" },
]);
// Filter students so that only students
// whose name starts with R and likes Vue is shown with likesVueAndStartsWithR
const likesVueAndStartsWithR = computed(() => {
// Remember .value !!!
return students.value.filter(
(student) =>
student.name.startsWith("R") && student.favFramework == "Vue"
);
}); // returns only: { name: "Rebecca", favFramework: "Vue" }
// don't forget to expose
return {
likesVueAndStartsWithR,
};
},
};
</script>
Vue A Note on Caching
<script>
import { ref, computed } from "vue";
export default {
setup() {
// Ref state
const students = ref([
{ name: "Susan", favFramework: "Vue" }, { name: "Samantha", favFramework: "React" },
{ name: "Rabi", favFramework: "React" },{ name: "Rebecca", favFramework: "Vue" },
]);
// Filter students so that only students
// whose name starts with R and likes Vue is shown
function likesVueAndStartsWithR() {
return students.value.filter(
(student) =>
student.name.startsWith("R") && student.favFramework == "Vue"
);
}
// don't forget to expose
return {
likesVueAndStartsWithR,
};
},
};
</script>
Vue Complex Conditional Logic
<script>
import { ref } from "vue";
export default {
setup() {
// Ref state
const students = ref([
{ name: "Susan", favFramework: "Vue" }, { name: "Samantha", favFramework: "React" },
{ name: "Rabi", favFramework: "React" }, { name: "Rebecca", favFramework: "Vue" },
]);
// Given a student, Return true or false depending on a condition
function doesLikeVueAndStartsWithR(student) {
return student.name.startsWith("R") && student.favFramework == "Vue";
}
// don't forget to expose
return {
students,
doesLikeVueAndStartsWithR,
};
},
};
</script>
Vue Watchers and Working With Side Effects
Example
<script setup>
import { ref, watch } from "vue";
const question = ref("");
const answer = ref("Questions usually contain a question mark. ;-)");
// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf("?") > -1) {
answer.value = "Thinking...";
try {
const res = await fetch("https://yesno.wtf/api");
answer.value = (await res.json()).answer;
} catch (error) {
answer.value = "Error! Could not reach the API. " + error;
}
}
});
</script>
Vue Deep Watchers
<script>
import { ref, watch } from "vue";
export default {
setup() {
// Ref state
const counts = ref([0, 1, 2]);
// Adds a new value to counts
function addToCounts() {
counts.value.push(counts.value.length + 1);
}
// Watch changes in counts
// Print out newCounts and oldCounts
watch(
counts,
(newCounts, oldCounts) => {
console.log("The old counts are: ", oldCounts);
console.log("The new counts are: ", newCounts);
},
{ deep: true }
);
// don't forget to expose the function as well.
return {
counts,
addToCounts,
};
},
};
</script>
Vue Lifecycle Hooks - onMounted and onUnmounted
<script>
import { onMounted, onUnmounted } from "vue";
export default {
setup() {
onMounted(() => {
console.log("My component has mounted");
});
onUnmounted(() => {
console.log("My component has unmounted");
});
},
};
</script>
Wait…isn’t this the same as React Hooks?
“We acknowledge the creativity of React Hooks, and it is a major source of inspiration for Composition API. ”
Vue’s Composition API vs React’s Hooks
Dangers
Dangers
Vue <script setup> - What does it do?
Vue <script setup> - Example
<script setup>
import { ref } from "vue";
import View from "./View.vue"
// Ref state
const count = ref(0);
// Increment count
function increment() {
// remember .value!!!
count.value++;
}
// Woah...no need to return anything!
</script>
<template>
<div>
<View
:count="count"
@increment="increment"
/>
</div>
</template>
Vue defineProps and defineEmits
<script setup>
// Props without typing
const props = defineProps(["foo"]);
// Props with typing
// const props = defineProps({ bar: String });
// Define names of all emit
const emit = defineEmits(["customEventName", "customEventWithParams"]);
function customEventName() {
emit("customEventName");
}
// Emit custom events with paramaters
function customEventWithParams(params) {
emit("customEventWithParams", params);
}
</script>
Vue MVP with Script Setup
<script setup>
// Must import the View
import ExampleView from "./ExampleView.vue";
const someComponentState = ref(0);
function myFunction() { console.log("Yay!"); }
function myFunction2(params) {
someComponentState.value = params;
}
</script>
<template>
<div>
<ExampleView
:foo="someComponentState"
@customEventName="myFunction"
@customEventWithParams="myFunction2"
/>
</div>
</template>
<script setup>
// Props without typing
const props = defineProps(["foo"]);
// Define names of all emit
const emit = defineEmits(["customEventName", "customEventWithParams"]);
// Emits custom event called “customEventName”
function customEventName() {
emit("customEventName");
}
// Emits custom event called “customEventWithParams” with params
function customEventWithParams(params) {
emit("customEventWithParams", params);
}
</script>
<template>
<div>
<p>{{ props.foo }}</p>
<button @click="customEventName">Click One</button>
<button @click="customEventWithParams('someVal')">
Click Two
</button>
</div>
</template>
Vue MVP using Both
Composition API and Options API
<script setup>
// Must import the View
import ExampleView from "./ExampleView.vue";
const someComponentState = ref(0);
function myFunction() { console.log("Yay!"); }
function myFunction2(params) {
someComponentState.value = params;
}
</script>
<template>
<div>
<ExampleView
:foo="someComponentState"
@customEventName="myFunction"
@customEventWithParams="myFunction2"
/>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: ["foo"],
// Define names of all emit
emits: ["customEventName", "customEventWithParams"],
methods: {
customEventName() {
this.$emit("customEventName");
},
customEventWithParams(params) {
this.$emit("customEventWithParams", params);
},
},
});
</script>
<template>
<div>
<p>{{ foo }}</p>
<button @click="customEventName">Click One</button>
<button @click="customEventWithParams('someVal')">
Click Two
</button>
</div>
</template>
Vue <script setup> - Limitations
For More Vue Information…
Vue Options API JSX Example
// Presenter.vue
<script lang="jsx">
// Line can be omitted if using auto-imports
import { defineComponent } from "vue";
import View from "./View.vue"
export default defineComponent({
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
render() {
return (
<View count={this.count}
onIncrement={this.increment}/>
);
},
});
</script>
// View.vue
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: ["count"],
// Define names of all emit
emits: ["increment"],
methods: {
increment() {
this.$emit("increment");
},
},
});
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment()">
Click</button>
</div>
</template>
Composition API JSX Example
// Presenter.vue
<script lang="jsx">
// Line can be omitted if using auto-imports
import { ref, defineComponent } from "vue";
import View from "./View.vue"
export default defineComponent({
// If using props or emits
// setup(props, { emits }) {
setup() {
const count = ref(0);
function increment() {
count.value++;
}
// Remember to return to expose all relevant state, functions, etc
return { count, increment };
},
render() {
return (
<View count={this.count} onIncrement={this.increment}/>
);
},
});
</script>
Pinia
Application State store for Vue
Pinia (Or in other words, Vuex 5)
// In main.js
import { createPinia } from "pinia";
app.use(createPinia());
Store Initialisation
// ./src/stores/counterStore.js
import { defineStore } from 'pinia'
import { ref, computed } from "vue";
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const name = ref('Hello World!')
// Derived State
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
// if you application state, derived state or actions to be available you must return it
return { count, name, doubleCount, increment }
})
Pokemon Explorer
// ./src/stores/pokemon.js
import { ref, computed } from "vue";
import { defineStore } from "pinia";
export const usePokemonStore = defineStore("pokemon", () => {
// State
. . .
// Actions
. . .
// Async Actions
. . .
// Getters / Derived state
. . .
// if you application state, derived state or actions to be available you must return it
return {
currentPokemon,
currentPokemonDetails,
error,
currentPokemonAction,
currentPokemonName,
};
});
State
// State
const currentPokemon = ref(null);
const currentPokemonDetails = ref(null);
const error = ref(null);
. . .
// remember to return it!
return {
currentPokemon,
currentPokemonDetails,
error,
...
};
Getters
// Getters or Derived state
const currentPokemonName = computed(() => {
if (currentPokemonDetails.value) {
return currentPokemonDetails.value.name.toUpperCase();
}
return null;
});
// remember to return it!
return {
...
currentPokemonName,
};
Actions
// Actions
function setPokemonId(id) {
currentPokemon.value = id;
}
function setPokemonDetails(details) {
currentPokemonDetails.value = details;
}
function setPokemonError(e) {
error.value = e;
}
// remember to return it if you want it accessible!
return {
...
};
Async Actions
async function currentPokemonAction(id) {
if (id <= 0) {
throw "Invalid pokemon id";
}
const pokemonId = id;
setPokemonId(pokemonId);
setPokemonDetails(null);
setPokemonError(null);
try {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/" + pokemonId
);
const data = await response.json();
if (currentPokemon.value === pokemonId) {
setPokemonDetails(data);
}
} catch (e) {
setPokemonError(e);
}
}
// remember to return it!
return {
currentPokemonAction,
...,
};
Watching State Changes in the Store
<script setup>
import { usePokemonStore } from "@/stores/pokemon";
import { ref, watch, onMounted, onUnmounted } from "vue";
const store = usePokemonStore();
const unwatch = ref(null);
onMounted(() => {
unwatch.value = watch(
() => store.currentPokemon, // What to watch:
() => { // What to do when it changes:
console.log("Pokemon changed");
// setPokemonInFirebase(store);
},
{ immediate: true } // Options
);
});
// onMounted was just used for illustrative purposes, you may want to stop subscribing
// if a user signs out
onUnmounted(function(){
if (unwatch.value) unwatch.value();
});
</script>
<template>
<App/>
</template>
Persisting the Pinia Store
2. Listen to the persistence and dispatch actions!
Multiple Stores
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUserStore = defineStore("userStore", () => {
const currentUserName = ref("My user");
function setCurrentUser(newName) {
currentUserName.value = newName;
}
return { currentUserName, setCurrentUser };
});
Access Other Store’s State
import { useUserStore } from "@/stores/userStore";
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useDinnerStore = defineStore("main", () => {
const currentDishName = ref("pizza");
const combinedGetter = computed(() => {
const userStore = useUserStore();
return userStore.currentUserName + " is eating " + currentDishName.value;
});
return { currentDishName, combinedGetter };
});
Accessing the Store’s State and Actions
import NumberEditor from "../views/numberEditor.jsx";
// Import the store
import { usePokemonStore } from "@/stores/pokemon";
export const PokemonIdPresenter = {
setup() {
// Activate the store
const store = usePokemonStore();
return function renderACB() {
return (
<NumberEditor
number={store.currentPokemon}
setNumber={store.currentPokemonAction}
/>
);
};
},
};
Accessing the Store with Object Destructuring
import PokemonView from "../views/pokemon.jsx";
import { propsNoData } from "../views/propsNoData";
import { usePokemonStore } from "@/stores/pokemon";
import { storeToRefs } from "pinia";
export const PokemonDataPresenter = {
setup() {
const store = usePokemonStore();
// In order to extract app state, storeToRefs is required
const { currentPokemon, currentPokemonDetails, error } = storeToRefs(store);
// Actions can be extracted via object destructuring
const {currentPokemonAction} = store;
return function renderACB() {
return (
propsNoData({
promise: currentPokemon.value,
data: currentPokemonDetails.value,
error: error.value,
}) || <PokemonView poke={store.currentPokemonDetails} />
);
};
},
};
Pinia Flow
Pinia Store
Presenter
View
Actions
State
Props
Custom Event
Old slides
Cloning arrays/object properties (this.someArr) when communicating to code outside the class
class Garage{� constructor(carArray){ this.cars= [...carArray]; } // avoid sharing carArray
getCars(){ return [...this.cars];} // avoid sharing this.cars
addCar(car){ this.cars= [...this.cars, car]; } ,
removeCarsOfMake(brand){ /* as before */ } ,
}
const myGarage= new Garage(someCarArray);
// or object version
const myGarage={
cars: [... someCarArray] , // avoid sharing someCarArray
getCars(){ return [...this.cars]; }, // avoid sharing this.cars
addCar(car){ /* as before */ } ,
removeCarsOfMake(brand){ /* as before */ } ,
};
“Removing” from arrays with filter(CB)
function isFerarriCB(car){ return car.make==="Ferrari"; }
cars.filter(isFerarriCB) // [Dino, Testarossa]
cars.find(isFerarriCB) // Dino as object, not array!
function keepOneMake(carArray, brand){
function isMakeCB(car){ return car.make===brand; }
return carArray.filter(isMakeCB);
}
keepOneMake(cars, "Ferarri") // [Dino, Testarossa]
const arr= [1, 7, 5, 3];
function keepNumbersOver(array, minimum){ // define function on the spot:
return array.filter(function overMinimumCB(elem){ return elem > minimum; });
}
keepNumbersOver([1, 7, 5, 3], 3) // [7,5]
arr.filter(CB)
arr: [ a, b, c, d, ]
CB(a,0,arr) CB(b,1,arr) CB(c,2,arr) CB(d,3,arr)
✅ (truthy) ❌ (falsy) ❌ ✅
result:[ a, d, ]
CB(element, index, array) means that the CB function is invoked (called back) by filter
The CB invocations can be executed in parallel (e.g. on multicore processors)
If CB is only defined with one parameter (element), it will ignore the other two (index, array), but filter always sends them!
Classes, methods, this , objects with methods
class Garage{�
addCar(car){ this.cars= [...this.cars, car];}
removeCarsOfMake(brand){
function carNotOfBrandCB(car){ return car.make !== brand; }
this.cars= this.cars.filter(carNotOfBrandCB);
}
}
const myGarage= new Garage();
Object version, using object literal. Note the commas ,
const myGarage={
addCar(car){ this.cars= [...this.cars, car];} ,
removeCarsOfMake(brand){ /* as above */ } ,
};
// How can we make other such objects? Object clone with object spread operator:
const anotherGarage= {... myGarage }; // same methods, and start with the same cars!
Idiom 1: call the observer when it is added (optional)
Typical Observer code. The red code snippets are typically identical!! Code repetition…
/* 1. read data from the model */
model.addObserver(function myObserverACB(){
/* 2. read latest data from the model */ ;
});
Idea: addObserver calls the observer once at addition. Then the code is simpler:
model.addObserver(function myObserverACB(){
/* read data from the model */ ;
});
Idiom 1 is is supported by Redux store.subscribe(), many ReactiveX subjects etc.
Idiom 2: return observer removal at addition (optional)
Usual addObserver/removeObserver sequence:
function myObserverACB(){..}
model.addObserver(myObserverACB);
Later in the code:
model.removeObserver(myObserverACB);
If we know that addObserver returns a removal callback, we can write:
const removeObserver= model.addObserver(function myObserverACB(){..});
Later in the code:
removeObserver();
Idiom 2 is is supported by Redux store.subscribe().
Other ways to implement Observable (aka Subject)
notifyObservers(event) can send a parameter to all observers, describing what has changed (aka event, or payload) in the Observable (e.g one more element in dishes). Not a GUI event!
JavaScript Proxy (used by Vue) can notify us about object property changes (also about reading properties!).
RxJS is a library that performs advanced operations with observables. Used a lot with Angular.
const mySubscription= observable.subscribe(console.log); // like addObserver
model.setNumberOfGuests(5)
observable.next(model); // like notifyObservers(), will log the model on console.log
const secondSubscription= observable.subscribe(console.log); // like addObserver
model.setNumberOfGuests(3);
observable.next(model); // like notifyObservers(), will log on console.log and console.error
mySubscription.unubscribe() // like removeObserver
new rxjs.Observable(function subscribeACB(s){
model.addObserver(function observerACB(){ s.next(model);});
})
Redux store is a multicast Observable (addObserver is called subscribe() ).
“Set search text”
“Set search dish type”
“Search now!”
SearchFormView
“Search result chosen”
SearchResultsView
searchResults
dishTypeOptions
DinnerModel (application state)
searchResultsPromiseState
onSearch(params)
Component State (framework dependent!) allows us to keep some data inside the component. Component state “survives” between one render and the next.
In TW3 you will first move the searchText and searchQuery in from application state (model) to component state.
Disadvantage 1: framework dependent (not a big problem for two strings)
Disadvantage 2: if you want to persist the latest search parameters in the cloud (to be available next time the user logs in) you will need to save them in application state anyway!
setCurrentDish(id)
Search
searchType, searchType
Application State combined with Component State
State changes => UI updates. Deep vs Shallow state check
Frameworks only implement UI update on Component State change!
But that can simulate application state.
VueRoot (simulates application state)
DinnerModel as component state
App
model
Sidebar
model
model
Summary
Details
model
Search
model
ReactRoot
DinnerModel fixed component state => no update!
App
model
model
model
Search
promiseState
model
Vue component state check is deep: any update in any state property (model.guests) or sub-object (dish array, promiseState) will result in update of relevant parts of UI= App (Sidebar, Summary, just Details, just Search etc)
Sidebar
guests
dishes
Summary
guests
dishes
Details
guests
dishes
currentDish
model
React state check is shallow. Any changes of state object content is ignored. The whole object (or string, number) has to change. Changing the box vs changing the content of the box.�The whole component updates, not just relevant parts.
notify! �Observer
react-redux
Old Update approach: incremental update
In the past, the approach was to render a GUI, and, on user interaction, perform incremental update, i.e. change only the parts of the GUI that need to be changed. So the programmer had to write:
Components were not easy to define if the render and update were written in different languages…
This approach is still used somewhat in Web Components (old, pre-framework approach, still trying to “take off”)
Modern Update: re-render, reconciliation
Modern interaction programming update is based on Re-render. That is, on every change, the component is re-rendered completely, even if only a small part of it changes. Now the programmer only needs to define:
Complete re-render is more resource-intensive than incremental update. Why re-render the whole component when only a small part changes?
To alleviate that, frameworks do not re-render in the browser UI tree (DOM) but in a virtual, in-memory tree (Virtual DOM).
Then they compare the two trees and only implement the differences in the browser DOM (Reconciliation)
Component State and state-based Update (React, Vue, …)
Custom components render and re-render by executing a function
Problem: function parameters and variables are “lost” after function execution (apart from closures)
Up to now we have used a kind of Application State
Frameworks only provide component state and we implement application state using that.
Up to now: pure Components (stateless)
Today: stateful components. They are not pure:
Most Views in the lab are pure components (stateless). Sometimes it makes sense for Views to have state though.
Most React Presenters have component state. Vue Presenters often don’t need it (due to Vue deep state check, e.g. lab Sidebar and Summary Presenters)
React Component State
(state hook, React state is shallow, React eager to re-render)
State and state-based Update in React
React State Hook (useState):
function ReactStatefulComponent(props){� const [value, setter]= React.useState(initialValue);
function handleEventACB(e){ setter(newValue); }
return <SomeTag someAttr={value} onSomeEvent= { handleEventACB } />�}
Or without JavaScript array destructuring
const arr= React.useState(initialValue);
const value= arr[0]; �const setter= arr[1]; // function!
re-render
React State: immutable Arrays and Objects
“changing state_var (calling setter with a different value) triggers re-render.”
React does this shallow for Arrays and Objects in component state. Adding an element to an Array (or changing a property of an Object) will not trigger re-render.
const [dishes, setDishes]= React.useState([]);
dishes.push(someDish); setDishes(dishes) // NO update! State is the same!
setDishes(dishes.concat(someDish)) // update! State has changed (a new array)
setDishes([...dishes, someDish]) // update! State has changed (a new array)
setDishes([someDish, ...dishes]) // update! State has changed (a new array)
You always need to create new Array or Object if their properties or elements change.
That means that the content of the Arrays and Objects in the React state should never change! They are immutable. If you want to change React state, you have to create new ones.
Vue Component State
(deep state check, selective update)
State and state-based Update in Vue
Vue:
const VueStatefulComponent={� data(){ return { value:initialValue }; }, � render(){
function myACB(e){ this.value= newValue;}
return <SomeTag someAttr={this.value} onSomeEvent= {myACB.bind(this)} />;�};
re-render
data(), render() and all other Vue component methods are actually ACBs
SomeComponent
<SomeComponent prop=val/> or
createElement(SomeComponent, props)
first render
Render
Functional: SomeComponent(props)
Object: render()
Component State changed. Update!
Component Vs Application State
The trend is to move a lot of the state in Application State
Typically Models are more minimalistic than Application States. They are often not aware of “users”, therefore no “current dish examined by user”, no dish searches by user etc. DinnerApplicationState would thus be a better name for DinnerModel
Vue component state check is deep, React check is shallow
Vue goes deep in all the Objects and Arrays in the state, and if something changes, it will update the components that depend on it
E.g. props.model from lab
Do not e.g. sort Arrays in Vue state, as that will cause re-render. Since typically sort is required by the UI, it may be required again during render. Infinite loop!
Do the sorting in the UI instead, on a copy of the Model array.
React only looks whether the Object or Array reference has changed (shallow state) and thus needs to encourage immutable state (which is a very good practice anyway)
React Fixed State sometimes useful. Force Re-render
const [neverChanges /* no setter! */]= React.useState({});
This ensures that neverChanges (typically an object) is associated with the component. Creating more copies of the component will also create their own fixed state. That’s useful!
But how to update, since the state can’t change?
const [/*ignored*/, setState]= React.useState();
To force re-render in e.g. an event callback: setState(new Object())
Presenter as Observer (lifecycle motivation example)
An Observer component needs to:
The Component State mechanism is not suitable for (1)
though something can be hacked (a boolean that records “did I register myself as observer”)
Component State cannot help at all for (2)
For this, we need another mechanism called Lifecycle
The mechanisms can be used for any kind of subscription, not just Observer
SomeComponent
<SomeComponent prop=val/> or
createElement(SomeComponent, props)
(0) first render
Render
Functional: SomeComponent(props)
Object: render()
(1.1), (1.2), (1.3) … Component State changed. Update!
(1) A component of this type was created!
(2) Component was taken down
wasCreatedACB()
isTakenDownACB()
Lifecycle
React component state and its consequences on re-render
When the model notifies, component state may change and component re-renders. It will not always change!
If the numberOfGuests does not change in the model (and the model notifies observers for some other reason, like adding a dish), setNumber will not lead to React re-render.
That is, if number is 42, then setNumber(42) will not re-render the component.
Therefore using the payload in a React observer-presenter is not needed (therefore unit tests in the lab do not send a payload to the presenter)
Warning: Using in JSX model data like {number} or {props.model.numberOfGuests} is the same because number is a copy of props.model.numberOfGuests
But stick to only one of them! Otherwise confusing race conditions occur!
Recommendation: the props.model form makes Vue presenter code from TW1 and TW2 directly re-usable!
Simplifications
function ObserverPresenter(props){
const [/*(a) ignore!*/, setNumber]=React.useState(/*(b) no value, see initialization below! */);
function observerACB(){ setNumber(props.model.numberOfGuests);}
function wasCreatedACB(){
observerACB(); // (b) call observer when adding it, to initialize the state
props.model.addObserver(observerACB); // 1. subscribe
return function isTakenDownACB(){ props.model.removeObserver(observerACB);} // 2.unsubscribe
}�React.useEffect(wasCreatedACB, []);// stricter: [props.model] but that never changes
// the JSX code is the same as in any props.model (Vue, TW1, TW2) presenter:
return <MyView someProp={props.model.numberOfGuests}
someEvent={function handleEventACB(e){props.model.setNumberOfGuests(x)}} />
}
Observer simplifications by Observable idioms (optional)
Remember Observer idioms 1 and 2
2. addObserver(obs) can return function(){ removeObserver(obs);}
const [number, setNumber]=React.useState(props.model.numberOfGuests);
function obsACB(){setNumber(props.model.numberOfGuests);}
function wasCreatedACB(){ return props.model.addObserver(obsACB); }�React.useEffect(wasCreatedACB), [])
1. addObserver(obs) can also call obs, so we don’t need to repeat props.model.numberOfGuests.
const [number, setNumber]=React.useState();
React observing some Promise State in Model
function PromiseStatePresenter(props){
const [, setPromise]=React.useState();
const [, setData]=React.useState();
const [, setError]=React.useState();
function observerACB(){
setPromise(props.model.somePromiseState.promise);
setData(props.model.somePromiseState.data);
setError(props.model.somePromiseState.error);
}
function wasCreatedACB(){
observerACB(); // initialization of component state
model.addObserver(/* TODO */);
return function nameACB(){/* TODO */}
}
React.useEffect(wasCreatedACB, []);
// the JSX code is the same as in any props.model (Vue, TW1, TW2) presenter:
return promiseNoData(props.model.somePromiseState) ||
<MyView someProp={props.model.somePromiseState.data} />
}
Can be combined with observing other props like numberOfGuests, dishes…
In the model, resolvePromise implements the notify parameter
Emphasized code can easily be turned into a custom hook (see next slide)
usePromiseState(
props.model,
"somePromiseState")
Subscribe/unsubscribe use case: simple navigation (TW3)
http://myServer/single/page/app#pageHash (TW3)
<NavPresenter><Child1/><Child2/></NavPresenter>
Goal: Child1 and Child2 should not be visible unless pageHash has a certain value. Otherwise NavPresenter will render
Where the hidden CSS class has the attribute display:none
The hash is available in browser DOM as window.location.hash . It is both readable and writable.
To detect changes to window.location.hash we subscribe to the window object’s "hashchange" event.
You can regard location.hash as a mini-model (it’s abstract data, just a string) and addEventListener as the way to add observers to it!
The hash was designed early on the Web to have URIs pointing to sections (fragments) of long documents that the user can navigate to without reloading the page.
Initializing a promise in React component state
E.g. the initial search promise in the lab
React.useState(searchDishes({})) will fetch every time the component is re-rendered!
You can use an effect with [] , to initialize at component creation. See next slide.
Or useState with a callback
const [promise, setPromise]= React.useState(function initializePromiseACB(){return searchDishes({})})
A few Component State remarks
React setX does not change X synchronously
const [model, setModel]=React.useState();
function initialEffectACB(){
// this is typically done in a bigPromise.then ACB:
setModel(someModel);
updateModelFromFirebase(model);
// will not work! At this point model is still undefined!
updateModelFromFirebase(someModel); // will work!
}
React.useEffect(initialEffectACB, []);
setModel only makes a plan for changing model but the execution continues with the old value (undefined in this case).
setModel triggers a re-render (like any state change). At that next render, model will have the new value (someModel)
Vue: this.X=obj is synchronous but this.X!==obj !!�
this.X is set to a special object called proxy, wrapped around obj
this.X is a reactive object: changing any property or sub-object property of X will update the UI (see VueRoot ever since TW1) and change properties of obj!
Changing any property of obj will not update the UI!!
created(){
function processPromiseResultACB(x){
// this is typically done in a bigPromise.then ACB:
this.rootModel=someModel;
updateModelFromFirebase(someModel);
// will not update the UI from firebase! No error in console though...
updateModelFromFirebase(this.rootModel); // will work!
}
somePromise.then(processPromiseResultACB.bind(this));
}
Vuex
Application State store for Vue
Vuex
Initialize store instance
const store = new Vuex.Store({
state:{..},
mutations:{..},
actions: {..}
});
Initial state
state: {
currentPokemon: null,
currentPokemonDetails: null,
error: null
}
Change state values
mutations: {
SET_POKEMON_ID (state, id) {
state.currentPokemon = id;
},
SET_POKEMON_DETAILS (state, details) {
state.currentPokemonDetails = details
},
SET_POKEMON_ERROR (state, e) {
state.error = e;
}
},
<script src="https://unpkg.com/vuex@next/dist/vuex.global.js"></script>
Vuex
Actions
actions: { // derived state!
currentPokemonAction(context, payload) {
const { commit, state } = context;
const pokemonId = payload;
if (pokemonId <= 0) {
throw 'Invalid pokemon id';
}
commit("SET_POKEMON_ID", pokemonId);
commit("SET_POKEMON_DETAILS", null);
commit("SET_POKEMON_ERROR", null);
return fetch("https://pokeapi.co/api/v2/pokemon/"+pokemonId)
.then(r => r.json())
.then(d=> {if(state.currentPokemon===pokemonId)commit("SET_POKEMON_DETAILS", d);})
.catch(e=> {if(state.currentPokemon===pokemonId)commit("SET_POKEMON_ERROR", e); })
}
}
commit: Update the state synchronously (Similar to simple Redux Action).
dispatch: Update the state sync or async by dispatching an action. Can contain many different commits. Similar to Redux-Thunk action creators.
store.dispatch("currentPokemonAction", 1); // init action
Vuex
// Connect component to store
const PokemonIdPresenter = {
computed: {
number() {
return this.$store?.state.currentPokemon;
}
},
methods: {
... // object spread syntax! mapActions() returns an object with actions as properties
Vuex.mapActions({
setNumber: 'currentPokemonAction'
})
},
render() {
return <NumberEditor number={this.number} setNumber={this.setNumber} />
}
}
// Providing the store
<script type="text/jsx">
const app = Vue.createApp(App);
app.use(store);
app.mount("#app")
</script>
HT22 A few remarks on lab tests and errors
152 tests, 220 active students, 3 lab weeks
14,561,224 tests (about 453 test runs per student, ~900 tests per group), 4,212,237 failed but:
467,082 "__webpack_require__(...).updateFirebaseFromModel is not a function" (should have been pending)
31320 "Cannot read properties of undefined (reading 'length')" total: 132,067
27650 "expected undefined to equal ''"
26976 "undefined is not a function" total is not a function: 98,125
14320 "Cannot read properties of undefined (reading 'replace')"
13327 "Cannot read properties of undefined (reading 'map')"
12639 "buttons[0].props.onClick is not a function"
12582 "promise should resolve to a model: expected undefined to be truthy"
11045 "Cannot read properties of undefined (reading 'children')"
3096 "resultArray.map is not a function"
2633 "setCurrentDishACB is not a function"
2513 "turnOff is not a function"
2489 "guestEvent is not a function"
1615 "dishChange is not a function"