1 of 381

DH2642 Interaction programming VT23

Cristian Bogdan

2 of 381

Cristian Bogdan, course leader, examiner

Edward Leander, amanuens, head TA

9 TAs, they have all gone through the lab tutorial before

3 of 381

Course Objectives

DH2642 Intended Learning Outcomes: Having passed the course, the student will be able to

  • choose appropriate technical platforms or JavaScript frameworks to create highly-useable data-persistent interactive web applications or native applications
  • program interactive web applications according to Model-View-Presenter or related architectures
  • program systems that read data from, and send data to, web APIs with good user experience
  • assess and improve the usability of existing interactive web applications
  • cooperate with others to implement interactive web applications.

See also grading criteria

4 of 381

Hederskodex

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.

5 of 381

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)

6 of 381

Examination

Labs 3 hp (P/F + advanced points)

  • Tutorial Lab, 3 weeks
    • pace.�isssues “queue” write your Zoom link (please no password)
  • TW1 individual
  • TW2-3 max 2 persons per lab group.
    • Join a TW2_TW3 group in Canvas ASAP
  • On Time delivery (by last commit) gives Bonus points
  • Advanced topics give Bonus Points, you can work on them any time during the whole of VT
  • 5 minute examination : talk briefly to a TA about the week learning objectives. Individual
    • TAs look at your code beforehand
    • The day(s) after the lab deadline
    • You will book using your Canvas ID in a spreadsheet. Instructions later
    • Show a Photo ID (passport, driving license)
    • If 5 min are not enough (they usually are!), we will contact you with longer time slots.

Project 4,5hp A-F (becomes course grade)

  • Labs are prerequisite for the project
  • Max 4 persons in project group
  • Grading criteria
  • Delayed project => lower grade
  • Presentation of selected projects at the end of the course. Obligatory attendance!

7 of 381

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)

8 of 381

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 SEhttps://canvas.kth.se/courses/37574/external_tools/3761

9 of 381

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.

  • Webpack setup (used a lot in industry, but we are considering a Vite setup)
  • UI (interactive) tests for each module (View, Presenter)
  • Unit tests , test-driven development (like the industry should work),
    • Unit: test each module in isolation,
    • Not a guarantee that the lab works correctly, but a good first step. You must check each UI test page!

VT23: tutorial v5.2

  • many test issues were fixed based on the HT22 experience
  • Tutorial was too verbose, too much text, it is being reviewed
  • Order of tasks changed to reflect learning objectives: Basic Rendering, Array Rendering, handle Native Events, fire/handle Custom events
  • Students use UI tests too little. UI tests linked from Unit tests
  • Stacktraces in the browser console instead of the Unit test page.

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.

  • one single slide deck with all lecture materials.
  • few links in the tutorial, search the concepts (usually mentioned in tutorial slide title or table headings) in Lectures or on the Internet.

10 of 381

11 of 381

Course Schedule

Demo of technologies mentioned in the course

https://www.csc.kth.se/~cristi/pokemon-explorer/

12 of 381

The minimum JavaScript you need in DH2642

13 of 381

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

14 of 381

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;

}

}

15 of 381

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]

16 of 381

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)

17 of 381

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

18 of 381

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("")

19 of 381

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); }

20 of 381

== 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)

21 of 381

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

22 of 381

Callbacks

The single most important concept in Interaction Programming

23 of 381

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.

24 of 381

Programming 101

My Code

Library

25 of 381

Interaction programming (among others)

My Code (callbacks)

Framework

Library (toolkit)

Inversion of control

Hollywood principle

Template method (e.g. sort)

26 of 381

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

27 of 381

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

28 of 381

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

29 of 381

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!

30 of 381

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!

}

31 of 381

Lab coding conventions (Obligatory)

  • All callbacks must have a name. JS allows anonymous callbacks (thick arrow functions or simply function(params){body} ), we do not use them in the labs!
  • The callback name and the parameter names must be original to you and show that you have understood their role
  • Callback names must end with CB or ACB

Why?

  1. Examination purposes (shows understanding, spares 5-min discussion time)
  2. Much easier to debug when the error / stack trace you get includes a function name instead of (anonymous) . Especially code assembled by webpack (which we use in the lab) can have large line numbers, very different from the line numbers in your source file, therefore the function name helps a lot! You need all the troubleshooting help you can get because you have no compiler.
  3. Many students learn callbacks in the course. This way is easier for them

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!

32 of 381

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

33 of 381

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

  • When we receive a callback, we are supposed to call when suitable (according to the logic our program), and are responsible to set its arguments! Custom event firing (TW1), React state setter (TW3)
  • When we pass a callback, we are not supposed to call it (sort, other higher-order functions like map, filter, reduce, setTimeout, and all the cases above)

34 of 381

Basic HTML, JSX

35 of 381

<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, HEAD, BODY
  • LINK, SCRIPT
  • DIV are like paragraphs (take whole width available to them)
    • Want a new “row”? Add another DIV!
  • SPAN are like words (take only width necessary, and flow to the next line if there’s no space left)
  • BUTTON
  • INPUT
  • SELECT
    • OPTION
  • A (anchor, i.e. hyperlink)
  • TABLE, THEAD, TBODY, TR, TD

HTML Element attributes

  • class CSS style class
  • A href="http://someURL"
  • SPAN title (many elements can have title)
  • INPUT placeholder, value, defaultValue, size
  • SELECT size, …
  • IMG src="http://someURL"

36 of 381

Graphical user interfaces represented internally as trees

DIV classBUTTONClick 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

37 of 381

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.

38 of 381

Troubles with HTML

  1. Cannot have variables or parameters
  2. Cannot “repeat” UI without repeating code
    1. Subprograms (see Custom Components later)
    2. Loops (see Array Rendering later)
  3. Cannot show UI conditionally (sometimes shown, sometimes not, depending on a condition)

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:

  • HTML templates in Angular, Vue {{ variable}} � <DIV v-for=”i in items” > repeated HTML using {{i}} </DIV>
  • JSX (JavaScript XML). Invented by React, used by many others, notably Vue since version 2

39 of 381

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

40 of 381

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

41 of 381

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"));

42 of 381

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>

  • sometimes referred to as native component
  • disabled is a HTML attribute but in JSX it’s sometimes referred to as prop as well

43 of 381

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!

44 of 381

TW1.2 Basic rendering

DIV BUTTON disabled-numberBUTTON+

DOM tree

number

SidebarView

Learning goal

File name

Props (bold)

disabled

<button />

View we work on

Native element

Attribute

tw/tw1.2.1.js Vue React

UI test file, links:

Indentation: parentchild 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

45 of 381

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

46 of 381

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!

47 of 381

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

48 of 381

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

49 of 381

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>; }

  • Pasta
  • Pizza
  • Dolce

[{name:”Pasta”}, {name:”Pizza”}, {name:”Dolce”}] Arrray.map()

[<LI>Pasta</LI>, <LI>Pizza</LI>, <LI>Dolce</LI>] <ul>

50 of 381

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)

51 of 381

TW1.3 Array rendering, basic CSS

DIVBUTTON-numberOfGuestsBUTTON+TABLE� TBODY TR key (array rendering)TDBUTTONxTD� A href

dishName test/dishesConst.js to figure which dish properties to useTDdishType import necessary function from utilities!TD class same right-align CSS class as in SummaryView dishPrice multiply with the number prop! Exactly two decimals. TRTD (empty)� TD classTotal:TD (empty)� TD totalPrice import necessary function! multiply with number! Two decimals.

SidebarView

src/views/sidebarView.js

tw/tw1.3.js Vue React

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

52 of 381

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

  • ComplexComponent1 if condition is truthy,
  • ComplexComponent2 otherwise
  • Exactly one of ComplexComponent1 and ComplexComponent2 are evaluated (rendered)

Similar to condition ? <ComplexComponent1 prop={value} /> : <ComplexComponent2/>

53 of 381

Styling with CSS. Basics

Typographic styling

54 of 381

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

55 of 381

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

56 of 381

text-align (default left)

vertical-align (default baseline)

For e.g. images see:

height, width

Whatever typography options are available a Text editor, are also available in CSS. And more!

57 of 381

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

58 of 381

CSS in developer tools. CSS Box model

See also margin collapsing (happens on top or bottom of adjacent boxes)

59 of 381

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}

60 of 381

DOM (native) events

clicks, keys, change, input, focus, blur

61 of 381

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…

62 of 381

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} />

63 of 381

TW1.4 Handle native events

number

SidebarView

disabled

  • - <button />

click

tw/tw1.4.js Vue React

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.

64 of 381

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

65 of 381

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

66 of 381

Native event bubbling, capturing, low-level vs higher-level, debouncing

67 of 381

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.

68 of 381

More ways to produce click on a button

In most GUI technologies

  • click= mousedown followed by mouseup
  • click= keyboard focus followed by keydown with Enter or Space keys

Thus:

  • click is a “higher-level” event.
  • mousedown, mousemove, mouseup, keydown, keyup, focus, blur are “lower-level

Other high level: keypress (keydown+keyup) etc

The high-level event is sometimes fired just before the low-level event that produces it:

  • mousedown, click, mouseup
  • keydown, keypress, keyup

68

69 of 381

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()?

70 of 381

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.

71 of 381

JSX custom events

72 of 381

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:

  • handling its native events,
  • and, when doing so…
  • fire custom events.

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)

73 of 381

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!

74 of 381

TW1.4 Native events and firing custom events

number

SidebarView

disabled

  • - <button />

click

onNumberChange

Custom event fired by the custom component (view in this case)

Native element(s)

Native event fired by the native component (element)

tw/tw1.4.js Vue React

Graphical appearance (sketch)

src/views/sidebarView.js

Link to this slide guide

75 of 381

TW1.4 Handle native event, fire custom event

number

SidebarView

disabled

<a />

tw/tw1.4.js Vue React

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

76 of 381

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

  • are independent on the UI platform used. Instead of a native event object as parameter, the custom event gets some abstract data (a string in this case) as parameter
  • do not use any special JS or JSX mechanism. Simply pass a callback as a component prop (handling) and, in the component, call the received callback when appropriate. Sometimes referred to as callback props

Both custom and native events are referred to as just events in a lot of documentation sites. But the distinction is important

77 of 381

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

78 of 381

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

79 of 381

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!

80 of 381

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

81 of 381

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.

82 of 381

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

83 of 381

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:

  • book a 5 min slots (slots are available in a google sheet).
  • use your Canvas student ID to book (not name, PNR, KTH ID…). There will be instructions how to find that.
  • If 5 min are not enough to show that you master the TW1 concepts, you get 15 min later.

Bonus points for properly reported issues.

  • File your Issue so that we can help you based on the Issue text only. That way we may not even need to Zoom and you may get help earlier.
  • Relevant code snippet is usually what makes us be able to help :) (other students will be able to help as well! There is bonus for that too!)
  • You only get bonus if you properly summarize what was the problem and tell us what you learned, before closing the Issue

84 of 381

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.

85 of 381

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

86 of 381

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

87 of 381

View-Presenter separation

The graphical representations now need to

  • read data from the Model
  • Represent data
  • Detect user events
  • Interpret events and modify the model
  • When notified by the model, read data again and re-render

The graphical representations now need to

  • read data from the Model
  • Represent data
  • Detect user (native) events
  • Interpret events and modify the model
  • When notified by the model, read data again and re-render

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.

88 of 381

Model-View-Presenter

  • read data from the Model send props to View
  • Interpret (custom) events from View and modify the Model
  • When notified by the Model, read data again and re-render View

88

DinnerModel

numberOfGuests

addObserver/proxy

notify

addObs/proxy

notify

  • Represent data
  • Detect user (native) events

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.

89 of 381

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?

  • See src/vuejs/VueRoot.js, which uses Component State. We will discuss that later.
  • In TW3 we will implement the Observer mechanism, so a callback will be invoked in each presenter for notification
    • In other words, Vue has a built-in observer (JS Proxy), React does not

90 of 381

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

91 of 381

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

92 of 381

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

  • Presenter does the Controller job (change Model), and more.
  • A part of the Controller is in the View (listen to events, convert to custom events)

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

93 of 381

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

94 of 381

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”

  • "Summary for {{guests}} guests" One-way binding (aka interpolation) to the guests value stored in the ViewModel.
  • 'Enter number: <input type="text" v-model="number" />'will display the number value stored in the ViewModel, but will also be able to change it (which will lead to a Model modification by the ViewModel). This is two-way binding.

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

95 of 381

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

96 of 381

Layout with CSS

flexbox, grid

97 of 381

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.

98 of 381

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.

99 of 381

Web APIs

synchronous vs asynchronous, fetch(), Promises, elements of HTTP, some component lifecycle

100 of 381

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.

101 of 381

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)

102 of 381

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){..}

)

103 of 381

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

  • then() is a method of Promise
  • then() also returns a Promise (so another then() can come after, hence chaining)
  • catch() is also a promise method returning 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:

  • If things go well in fetch() and acb1 is called, we say that the promise resolves (or fulfills)
  • Otherwise the promise rejects (or fails) and errorCallbackACB is called.
  • If acb1 returns a Promise, that promise will be returned by then(acb1).
    • otherwise then() will wrap its result into a promise that resolves immediately (so the next then() is called, if any)

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

104 of 381

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

105 of 381

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

  • a response code (200 is “OK”, 404 is “not found”, etc)
  • response headers (e.g. response data length, how is the data represented, aka encoding) etc
  • But no data!

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); })

106 of 381

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

107 of 381

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

108 of 381

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)

109 of 381

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

}

110 of 381

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);

}

111 of 381

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);

}

112 of 381

async/await

Special JavaScript (and C#) syntax for Promises.

  • const x=await promise; means that the result of promise (if it resolves=fulfills) is set to the constant (or variable) x .
  • It “feels” to the programmer that await “blocks” until the Promise data comes but this will not lead to blocking the UI event treatment.
    • The part after the await becomes a callback internally
  • Any function that uses await must be declared async . It will return a Promise (can be used with .then(..).catch(..) or with await).
  • To treat errors (instead of .catch(er=>..)) you need to use JavaScript statements try{..}catch{..}

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)

113 of 381

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.

  • HTTP method
  • HTTP resource path (path on server) and query string
  • HTTP headers
  • HTTP body (data)

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

114 of 381

HTTP method and REST. API Endpoints

HTTP method is used by a convention called REST (REpresentational State Transfer) to decide the API action:

  • GET (read data from API). Default method in e.g. fetch()
  • POST (create new data on the server, or send complex read requests)
  • PUT (update data on the server)
  • DELETE (remove data from server)

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

115 of 381

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.

116 of 381

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.

  • The property names (HTTP header names) may contain strange characters like - so you must use quotes for the property name. The following header instructs the server that we will send the data in JSON format. {'Content-Type': 'application/json'}

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);} ); }

);

117 of 381

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!

118 of 381

async/await

Special JavaScript (and C#) syntax for Promises.

  • const x=await promise; means that the result of promise (if it resolves=fulfills) is set to the constant (or variable) x .
  • It “feels” to the programmer that await “blocks” until the Promise data comes but this will not lead to blocking the UI event treatment.
    • The part after the await becomes a callback internally
  • Any function that uses await must be declared async . It will return a Promise (can be used with .then(..).catch(..) or with await).
  • To treat errors (instead of .catch(errorACB)) you need to use JavaScript statements try{..}catch{..}

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)

119 of 381

Promises and User Interaction

Dealing with async data in interactive applications (suspense)

120 of 381

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

  • When promise is resolved (aka fulfilled, aka successful), data is set
  • When promise is rejected, error is set

promise changes due to user interaction (a new search, reading another dish)

When anything in the promise state changes, the UI will update

  • In TW2 we hang the promise state in application state (Model). Advantage: framework-independent code
  • Later we can use component state (framework-dependent)
  • or more specialized application state (three properties in a Redux store)

However the Promise state, and its issues, are similar.

121 of 381

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

122 of 381

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…

123 of 381

Remarks

Does debouncing resolve this problem? Debouncing would ensure that

  • we do not query for "p", "pa", "pas", "past"
  • but only for e.g. "pa", "past"
    • (depending how fast the user is typing)

So the problem may be alleviated somewhat but not strictly solved

The root of this problem is shared data

  • However, application state tends to be shared data, it’s difficult to avoid that.
  • Even if a new application state object would be created at every state change (immutable state, e.g. Redux), the fact that two different promises want to change the same parts of that state leads to the race condition

124 of 381

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);

}

125 of 381

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.

126 of 381

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.

127 of 381

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

128 of 381

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

129 of 381

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!

130 of 381

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

131 of 381

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!!!

132 of 381

Implementing Observer

JS (needed in TW3), RxJS, Other languages /platforms

  1. General architectural structure (Observer pattern)
  2. Implementation in JavaScript
  3. Demo

133 of 381

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

  • Example: moving from localStorage persistence to Firebase persistence. Model should stay unchanged
  • Moving from Firebase real-time-database to Firestore
  • Model does not even care whether the observers are graphical (like Presenters) or abstract objects (e.g. a PostScript generator to send the model data to a printer)

Vue implements advanced multicast notification + update, which is why we did not need Observer thus far

  • Keeping the model independent of persistence requires Observer though!

134 of 381

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)

135 of 381

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

136 of 381

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!

  • setNumberOfGuests(nr) not call notifyObservers() if nr is the same as before.
  • addToMenu(dish) do not make any change (and not call notifyObservers()) if the dish is already in the menu. This will avoid duplicates in dishes.
  • removeFromMenu(dish), do not call notifyObservers() if the dish is not in the menu.
  • setCurrentDish(id) not call notifyObservers() if id is the same as before. It must (already since TW2) not instantiate a promise if id is the same as before, as that would be a waste of network resources.

We do not notify observers on doSearch, setSearchQuery, setSearchType because the search state will be moved application state (model)component state of Search presenter

137 of 381

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

138 of 381

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)

139 of 381

Observer pattern implementations

JavaScript Proxy

  • Gives notifications of changes in any Object. Used by Vue for deep state check
  • Only unicast notification. Helps avoiding to rewrite the mutating methods (setNumberOfGuests, addToMenu…) but not the broadcast

Java

  • java.util.Observable class, extend it to create your own model
  • java.util.Observer interface (a single method notify() )

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

140 of 381

Component State

Comparison to Application State (model)

141 of 381

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, …)

142 of 381

“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)

143 of 381

“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

144 of 381

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

145 of 381

  1. If stateMember changes will Component re-render? Will SubComponent re-render?

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 does not use stateMember
  • SubComponent uses stateMember
  • in that case only SubComponent updates!
    • Example: App does not use props.model.numberOfGuests. Sidebar does. App never updates, only Sidebar / SidebarView.

Component render: <div><SubComponent prop={stateMember1}/> {stateMember2} </div>

146 of 381

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

    • Example: Vue detects automatically that Sidebar, Summary Details use the model.dishes and Search does not use it. Therefore when model.dishes dish changes, only Sidebar, Summary and Details update
    • TW1 and TW2 application state is actually the component state in the topmost component (see tw/VueRoot.js)

147 of 381

Application state as Vue top-level component state (TW1, TW2)

“Deep state check”

rootModel

  • numberOfGuests
  • dishes
    • dish 0
      • title
      • ingredients
    • dish 1 …
  • currentDish
  • currentDishPromiseState
    • promise
    • data
    • error

“Selective update”�Which components should update?

  • Sidebar, Summary, Details
  • Sidebar, Summary, Details
      • (Sidebar, Details)
      • (Summary, Details)
    • etc …
  • Details
  • Details
    • Details
    • Details
    • Details

148 of 381

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.

  • passed via props to sub-components
  • changes come via custom events from sub-components
  • “Props down, events up”

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:

  • Observer (in the lab),
  • Application State Stores (Redux, Recoil). You can jump directly to these in TW3:
    • Redux is a more advanced observable (store.subscribe is the same as model.addObserver)

App

numberOfGuests

dishes

currentDish

149 of 381

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

150 of 381

Component Lifecycle

Lets you register ACBs that run

  1. When the component is first rendered
  2. When the component is taken down
    • When are components taken down? For example when they are hidden during navigation (router)

What does this have to do with component state? Use cases:

  • Initiate the first search (only (1)). Search results will be kept in component state since they are only relevant for that Presenter-View
  • Subscribe (1) and unsubscribe (2) to a Model / Observable
    • Then copy relevant data from application state to component state
    • react-redux does this with the Redux store as observable
    • Failing to remove the observer will update a hidden component => big warning
  • Subscribe (1) or unsubscribe (2) to a browser event (making e.g. a router)
    • window.addEventListener("hashchange", ACB) .. removeEventListener..

151 of 381

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)

  • Vue deep state check will detect changes in data and error
  • React: force re-render when the promise resolves to address shallow state check

Component

promiseState

promise

data

error

forceRerender (React only)

152 of 381

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.

  1. the model current dish changed by Search or Sidebar, i.e. outside the control of Details!
    1. picked up by Vue deep state check
    2. or by React observer, which copies it in a state property: currentDish
  2. Details must initiate a promise to and resolve it in its promiseState

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

153 of 381

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

154 of 381

For each framework:

0: Styles of defining stateful components

  1. How to define component state members?

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?

  • How to register component lifecycle ACBs?

React example: how to add Observers and copy state from the Model to component state

  • How to register derived state ACBs?
  • Example: how to resolve promises in component state?
  • How to reuse state, lifecycle and derived state code? (aka custom hooks)

155 of 381

Vue: 0. Stateful component styles

APIs

  • Options API: Objects with a render() method. The data() method returns the initial state. State (and props) accessed as this.stateMember
  • Composition API. Objects with a setup(props) method.

Rendering

  • JSX, generates JS function calls (like in React)
  • Templates: strings that are closer to HTML, inherited from Angular.
    • Translated to JSX. The reverse is not always possible (JSX more powerful)
    • Options or Composition object property called template
    • Or Single File Components (SFC), .vue extension

156 of 381

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

157 of 381

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

  • numberOfGuests
  • dishes
    • dish 0
      • title
      • ingredients
    • dish 1 …
  • currentDish
  • currentDishPromiseState
    • promise
    • data
    • error

“Selective update”�Which components should update?

  • Sidebar, Summary, Details
  • Sidebar, Summary, Details
      • (Sidebar, Details)
      • (Summary, Details)
    • etc …
  • Details
  • Details
    • Details
    • Details
    • Details

158 of 381

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>;};

},

};

159 of 381

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

  • setup() runs before the first render
  • onMounted() (recommended!) runs just after the first render, when the component is visible (mounted)

Besides onMounted() and onUnmounted(), Vue supports other lifecycle hooks

160 of 381

Vue complete lifecycle

161 of 381

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!

162 of 381

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);

};

},

};

163 of 381

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 */ };

},

};

164 of 381

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 */ };

},

};

165 of 381

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>);

};

},

};

166 of 381

For each framework:

0: Styles of defining stateful components

  • How to define component state members?

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?

  • How to register component lifecycle ACBs?

React example: how to add Observers and copy state from the Model to component state

  • How to register derived state ACBs?
  • Example: how to resolve promises in component state?
  • How to reuse state, lifecycle and derived state code? (aka custom hooks)

167 of 381

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

168 of 381

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"/>

169 of 381

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.

170 of 381

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>;

}

171 of 381

React lifecycle diagrams (Hooks and old Class components)

172 of 381

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

173 of 381

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

174 of 381

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.

175 of 381

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)

  • unconditionally
  • always in the same order
  • typically in the beginning of your component code.

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.

176 of 381

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>;

}

177 of 381

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

Array destructuring,

Ignore first element

178 of 381

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:

  • Lifecycle begins (initial Search)
  • Custom event handler (Search)
  • Observer (Details, currentDish)

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

179 of 381

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.

180 of 381

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);

}

Array destructuring,

Ignore second element

181 of 381

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

182 of 381

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 ,.. }

183 of 381

Vue templates

184 of 381

Vue: 0. Stateful component styles

APIs

  • Options API: Objects with a render() method. The data() method returns the initial state. State (and props) accessed as this.stateMember
  • Composition API. Objects with a setup(props) method.

Rendering

  • JSX, generates JS function calls (like in React)
  • Templates: strings that are closer to HTML, inherited from Angular.
    • Translated to JSX. The reverse is not always possible (JSX more powerful)
    • Options or Composition object property called template
    • Or Single File Components (SFC), .vue extension

185 of 381

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>);

};

},

};

186 of 381

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.

187 of 381

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"

188 of 381

Vue template language

  • Any const defined in the script can be used in the template!
  • v-bind:attr="expr" dynamic HTML attribute, shortcut :attr
    • See :value attribute in the OPTION
    • attr="constant" (without v-bind:) is an ordinary fixed attribute
  • v-for instead of the JSX array rendering
  • v-if and v-else for conditional rendering, instead of the JS A && B or A || B.
    • v-for and v-if illustrate procedural instead of the JSX declarative constructs…
  • v-on:click for native events, shortcut @click
  • v-model is a combination of v-bind and v-on:input to allow for read and write of a value (double binding)
  • {{ expr }} is called interpolation. Has nothing to do with JSX { }. Unlike JSX, you cannot use further HTML/JS inside interpolations. This is why templates are inferior to JSX
  • props names are defined in defineProps
  • you can declare custom events as props in defineProps and fire them like props.customEventName(params) as usual
  • CSS in the vue file <style> section, so styling can stay close to the UI.

//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} />

189 of 381

Support for custom events handling in templates

If the presenter is written as template, custom events should be emitted by the View

  • defineEmits(["customEvent", .. ])
  • fire the event: emit("customEvent", params)
  • same as declaring onCustomEvent as prop and using: props.onCustomEvent(params)

(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" />`

190 of 381

A few Component State remarks

191 of 381

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:

  • we must define onInput or onChange
  • not doing so will result in a read-only INPUT: React will keep it in sync with the component state.
  • alternative: use defaultValue
  • onInput and onChange behave identically in React!

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

192 of 381

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.

193 of 381

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!!!

}

194 of 381

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

In the course: component state in Presenter, not in View. Keep the Views “dumb”!

195 of 381

Using component state and lifecycle for subscriptions (b, c)

Subscribing to a value (from Model or window.location.hash or …)

  • copy the value in component state,
  • when notified of mini-model value change, copy the value again into component state
    • that will make our component re-render when the value changes!

(mini)model value —> component state property

The model value is known as a “source of truth”

196 of 381

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

197 of 381

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]

198 of 381

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

199 of 381

Navigation: routers

200 of 381

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

201 of 381

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

202 of 381

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.

203 of 381

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>

204 of 381

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>

205 of 381

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>

206 of 381

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

  • Use application state for Search (lab)
  • Keep some state in the route. /details/:ID (later), /search?text&type
  • Vue keep-alive

<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!).

  • React <Offscreen/> not yet available! will be released as a minor version of react 18

207 of 381

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)

208 of 381

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', {}))

209 of 381

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.

210 of 381

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>

211 of 381

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!

212 of 381

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.

213 of 381

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

214 of 381

Firebase

Persist data in the cloud (lab)

Authentication and Data Security (project)

215 of 381

216 of 381

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!

217 of 381

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

  • Converts dishes to an array of IDs, sorted
  • If currentDish is undefined, set null, or don’t set anything

persistenceToModel(persistedData, model) returns promise

  • If the persistedData is falsy (it will be when there is no data in cloud), we assume an empty object
  • Set the number of guests and current dish in model
    • If the number of guests is not present in persistedData, assume them to be 2
  • Perform getMenuDetails on the dish ID array from persistedData when it resolves, replace the dishes array in the model . Return the promise
    • If dishes are not present in persistedData, assume them to be an empty array. getMenuDetails([]) should work (returns a promise that resolves to an empty array)

218 of 381

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!

219 of 381

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

220 of 381

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)

  • the key (ID) of the child is available as firebaseData.key
  • The value of the child is in firebaseData.val() as usual

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 of 381

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!

222 of 381

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!

  1. Model notifies Observers
  2. model Observer saves in firebase (set)
  3. Firebase notifies value, because it was changed by (2)
  4. Persistor saves the change in model (doChange)
  5. Model is changed again, notifies observers. Go to 1!!

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:

  • Only notify observers when the model has really changed
  • Send a payload when the notification is due to a change from the cloud
  • The persistence observer, when notified sees the payload and refrains from calling set

Persistor

(observer)

notify

value

set

doChange()

Model

223 of 381

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)

224 of 381

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

225 of 381

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

226 of 381

Application State

Model, Context, Redux, Pinia, Recoil

(whatever approach you use in TW3, Views stay as they are)

227 of 381

Context

React: Context, Vue: Provide/Inject

228 of 381

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.

  • searchParams -> searchResults
  • currentDish -> currentDishDetails
  • maybe also (synchronous) dishes -> ingredients

Component hierarchy !== Application State hierarchy and dependencies

(so top-level state has inherent issues in larger applications)

229 of 381

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>

230 of 381

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

231 of 381

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.

232 of 381

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!

233 of 381

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

234 of 381

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

  • pokemonIDAtom
    • pokemonIDState
      • pokemonData
  • pokemonIDError

235 of 381

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()

  • is needed because otherwise the app would briefly show the default value (1) initially, until the firebase on() fires.
  • Because it sets the value to a promise, the components that depend on the atom must be wrapped in a <Suspense>

236 of 381

TW3 in Recoil (example)

Atom and selector dependencies

  • numberOfGuestsAtom default 2
    • numberOfGuests (writable, don’t allow <1)
  • dishesAtom default []
    • dishes (writable, don’t allow duplicates)
    • ingredients?
  • currentDish default null
    • currentDishDetails (Promise)
  • searchParams default {}
    • searchResults (Promise)
  • error (set to null from writable selectors if value is ok)

Persist numberOfGuestsAtom, dishesAtom, currentDish to Firebase.

237 of 381

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

238 of 381

Redux

React-Redux, using Redux with Vue

239 of 381

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:

  1. most user events lead to dispatching one or more actions.
  2. the actions lead to store state change (by calling reducers)
  3. store state change leads to notification and UI update. Then the user can interact again etc.
  4. Before Flux/Redux complex MVC applications could happen to break this flow!
  5. See 2014 Video motivating Flux (Redux precursor) and React itself!

240 of 381

241 of 381

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)

242 of 381

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 ^^^

243 of 381

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

244 of 381

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

245 of 381

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!

  • NumberEditor knows nothing about Pokemons.
  • reducers.js knows nothing about NumberEditor or about mappings
  • mapToProps.js knows about both redux state and views. It is an adapter between the them, as all presenters are
  • All three are framework-independent!

246 of 381

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}));

247 of 381

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)

248 of 381

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;

}

249 of 381

250 of 381

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

251 of 381

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

252 of 381

Redux pros and cons

Easy to add new properties to the application state

  • Does currentDish really belong in the Model? Such questions go away
  • Programmers can work with their (groups of) reducers without disturbing each other

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:

  • boilerplate code (solutions: action creators, even reducer creators)
  • too complicated for simple apps
  • no specific features for async

253 of 381

Persisting the Redux state

(libraries like firebase-redux)

1. Listen to the Redux store using store.subscribe()

  • (reducers cannot have side effects, so you cannot persist from reducers)
  • save the desired state properties read from store.getState()
    • (e.g. firebase ref dinnerModel set on numberOfGuests, dishes, currentDish)

2. Listen to the persistence storage and dispatch actions!

  • E.g. firebase onValue()
    • "value" events on the ref "dinnerModel/numberOfGuests” will dispatch SET_NO_GUESTS
      • (or better an action creator that returns a SET_NO_GUESTS action)
    • "value" events on "dinnerModel/currentDish" will dispatch currentDishAction()
    • "child_added" events on "dinnerModel/dishes" will dispatch ADD_DISH (or action creator)
    • "child_removed" events on "dinnerModel/dishes" will dispatch REMOVE_DISH (or action creator)
  • Make sure that there is no state save at step (1) during dispatch of actions at step (2)
  • All these steps (and more techniques) can be found in this article.

254 of 381

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>

255 of 381

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>

256 of 381

Usability and User Experience (UX)

Concepts and discount methods

257 of 381

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

  • Interaction Design (IxD), more generative HCI
  • User experience (UX) includes Usability

258 of 381

Usability Heuristics, Heuristic evaluation

Nielsen

  1. Visibility of system status
  2. Match between system and the real world
  3. User (in) control and freedom
  4. Consistency and standards
  5. Error prevention
  6. Recognition rather than recall
  7. Flexibility and efficiency in use
  8. Aesthetic and minimalist design
  9. Help users recognize, diagnose, recover from errors
  10. Help and documentation

Schneiderman

  • Strive for consistency
  • Enable frequent users to use shortcuts
  • Offer informative feedback
  • Design dialogue to yield closure
  • Offer simple error handling
  • Permit easy reversal of actions
  • Support Internal Locus of Control
  • Reduce short-term memory load

259 of 381

How to quickly improve usability?

Without users: heuristic evaluation . Apply Nielsen/Schneiderman heuristics

  • Some heuristics are named in the project grading criteria. Users should
    • initiate actions rather than being told what to do (locus of control),
      • Understand where they are in the app, be able to “exit” the current path (control & freedom)
    • get feedback after performing an action

With users:

  • Cheapest method: Think aloud
  • Document the feedback/comments/reactions you got and the improvements (e.g. commits) you made based on the feedback
  • You have to involve users in the Lab (Think-aloud in TW3).
  • For a high Project grade, you should involve users in two stages:
    • at an early stage (formative evaluation), e.g. during prototyping
    • towards the end (but not when it’s too late)

260 of 381

Project preparation

261 of 381

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

262 of 381

Presenter-View separation

Use cases

263 of 381

Presenter-View recap

Presenter

  • read data from the Model send data as props to View
  • Interpret custom events from View and modify the Model
  • When notified by the Model, read data again and re-render View

View

  • Represent data
  • Detect user events and fire custom events

The Presenter is responsible with

  • giving the View the abstract (model) data it needs
  • giving meaning (semantics) to user interaction by changing the model

Presenter and View must co-vary independently. Similar to an Observer and the Observable (Model). Change one, the other stays the same.

  • Changing Presenter does not impact Model at all
  • Changing Model sometimes impacts the Presenter but to a low degree

“Language” motivation

View: lots of HTML, CSS (JSX), declarative, native events

Presenter: mostly JavaScript, data processing, possibly complicated, procedural processing

264 of 381

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.

265 of 381

What should the View send to the Presenter?

That is, what should be the Custom event parameters?

Presenter does not know about UI details.

  • Never send UI events from View to Presenter
  • Never send UI elements (like event targets)

Presenter is high-level (abstract, above UI details)

  • Send abstract, higher-level types: strings, integers, dishes, etc
  • Do not restrict objects when sending the parameter from View!
    • Do not go from dish to dish.id. It is up to the Presenter to decide what it needs from the larger object. So always send the entire object

Do not name the custom events in a UI fashion because Presenter is ignorant of the UI details

  • userClickedButton or userTypedInInputBox are not good custom event names.
    • You may decide to use a TextArea instead of an InputBox, that does not change the View-Presenter interface (custom events)
  • userWantsToSearch or userChangedSearchQuery are better

266 of 381

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:

  • When the dish type is changed. Simply modify changeTypeACB!
  • When the text query changes (search as you type). Modify changeTextACB but make sure not to trigger too many network accesses (debouncing: lodash, underscore.js, or handmade)
    • In Vue, you may need to change onChange with onInput in the view though. In React they do the same thing

Sometimes you will have to touch the view though

  • Remove the Search button (becomes useless if search is triggered by the input box or type select)
  • If you want to make a difference between onInput and onBlur / onChange you need to change the DOM event listeners in the View

267 of 381

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

  • With Redux, you can also generate the presenter automatically using mappings
    • see React-Redux,
    • but this can also work with Vue or even with no framework

268 of 381

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:

  • Select mode: If the onDragStart is on an existing shape move that shape to onDragTo
  • 3 shape modes: create a segment/rectangle/ellipse starting on onDragStart, with the height and width given by the current onDragTo
  • All modes: if onDragCancel is called, undo the action

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)

  • draws the shapes (converts from geometric coordinates to screen coordinates)
  • fires the four custom events.
    • passes the point parameters converted from screen coordinates to geometric coordinates.

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);

}

}

269 of 381

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

  • this appears convenient because then the v-bind and v-model works from the View directly on the Presenter state.
  • at the project this counts as mixing View and Presenter concerns and leads to a lower grade. Talk to your project coach!
  • there is a relationship here with the M-V-VM architectural pattern. However, the Vue implementation of M-V-VM seems to break the View-Presenter (ViewModel) separation.
    • In other words you should not use M-V-VM as an excuse to mix View and Presenter

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.

270 of 381

Project preparation

271 of 381

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)

  • npm install react, react-dom (or vue)
  • for JSX, the tools install Babel configured for the respective framework (in the lab we used the default React Babel config and plugged Vue’s when needed)
  • npm install webpack and the needed plugins
  • configure packages.json and webpack.config.js

create-react-app and vue-cli also perform a git init.

  • so you can already commit your changes but you cannot push
  • because there is no repository (origin)
  • You need to create a repo at github or KTH git.
    • (no README to avoid conflicts on first push)
  • git remote add origin yourRepo; git push origin
    • origin is just a convention, you can call it whatever...

272 of 381

Vite

  • https://vitejs.dev/

273 of 381

create-vue

  • Quick Start | Vue.js and Vue Router
  • npm init vue@latest
  • cd <your-project-name>
  • npm install
  • npm run dev

274 of 381

create-vue

275 of 381

create-react-app with Vite

  • Getting Started | Vite and React Router
  • npm create vite@latest name-of-your-project -- --template react
  • cd <your new project directory>
  • npm install react-router-dom localforage match-sorter sort-by
  • npm run dev

276 of 381

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

277 of 381

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***

278 of 381

Jsx in Vue

  • If it was not enabled in initial project creation, refer to: vitejs/plugin-vue-jsx
  • The file extension must be “.jsx” file so that it can be detected
  • defineComponent enables Hot Module Replacement (HMR)

// SomeComponent.jsx

import { defineComponent } from "vue";

export default defineComponent({

setup(props) {

return function renderACB() {

return <div>Hello World</div>;

};

},

});

279 of 381

Create a new repository on e.g. https://gits-15.sys.kth.se/

280 of 381

Create a git repository

  • If you have committed locally, don’t initialize with a README to avoid conflicts at first push
  • Be sure that your repository is Public

281 of 381

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

282 of 381

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

283 of 381

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

284 of 381

ESLint & Prettier

  • yarn add --dev --exact prettier
  • yarn add --dev eslint-plugin-prettier
  • Install plugins

285 of 381

ESLint & Prettier

  • VS Code user settings (shift + cmd + p -> Preferences)
  • Format on save

286 of 381

ESLint & Prettier

  • VS Code user settings (shift + cmd + p -> Preferences)
  • Format on save

287 of 381

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

288 of 381

API:s

  • Search and find an API
  • Allow CORS: Access-Control-Allow-Origin browser plugin

289 of 381

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

290 of 381

Coaching

  • Project coach assigned to each group.
    • Schedule meetings with your coach for feedback/help.
    • Meet your coach at least once a week
  • Submit a Project Proposal ASAP, you may get some feedback from the coach.

291 of 381

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

292 of 381

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

293 of 381

Composables and Hooks

294 of 381

Composables (Vue) and Hooks (React)

  • reusable utility functions
  • recommended for advanced applications
  • Libraries for Composables / Hooks can help solve common problems in advanced applications

However,

  • Make sure you still follow MVP, check with your coach
  • In general, a Composable / Hook should ONLY be in the Presenter
  • Certain Composables / Hooks are suitable for the View, however they should ONLY deal with events, css, etc

295 of 381

VueUse

  • VueUse is a collection utility functions, or Composables, based on Composition API
  • https://vueuse.org/
  • npm i @vueuse/core

296 of 381

React-Use

  • React-Use is a collection utility functions, or Hooks
  • https://github.com/streamich/react-use
  • npm i react-use

297 of 381

useHooks

  • useHooks contains React Hook recipes
  • https://usehooks.com/
  • Hooks must be added by developer

298 of 381

Recommended Composables / Hooks

VueUse

  • useFetch
  • useAsyncState
  • useToggle

React-use

  • useAsync
  • useAsyncFn
  • useToggle

useHooks

  • useAsync
  • useToggle

299 of 381

DH2643 Advanced Interaction Programming

300 of 381

Vue - Nuxt

  • https://nuxt.com/

301 of 381

React - Next

  • https://nextjs.org/

302 of 381

What they do?

  • Server Side Rendering (SSR)
  • Static Site Generation (SSG)
  • Typescript support
  • Intelligent defaults for production code
  • Provides framework for how to organise Folder directory
  • And much more…

303 of 381

Advanced Vue Concepts

304 of 381

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>

  • SFC (eg. *.vue files) is a special format that encapsulates template, logic and styling
  • Respectively: <template>, <script> and <style>

  • <template> includes html and Vue directives
  • <script> includes Javascript
  • <style> includes css. It is important to included “scoped” so that the CSS applies only to the given file

Warning:

  • Must follow conventions described in “Vue templates in the project

// 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>

305 of 381

Advanced Vue - Composition API

306 of 381

307 of 381

Which to Use?

Options Api

  • Easier for beginners
  • Enforces code organisation
  • Centered around “component instance” →“this”, thus easier when coming from OOP background
  • Code reuse is extremely difficult

Ideal for low to medium level complexity applications and for beginner developers.

Composition Api

  • Requires extensive Javascript, Reactivity and Lifecycle hook understanding
  • Free form code organisation
  • Flexibility - able to organise code by “concern” rather than “type” eg. sign in, sign up vs data, methods
  • Code reuse through use of Composables
  • Better Typescript support

Ideal for high complexity applications and for intermediate/advanced developers

308 of 381

Vue Composition API

  • setup() required when using composition api
  • Must return everything that wants to be exposed in the template/jsx
  • FAQ

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

309 of 381

Vue Reactive and Mutating State (DISCOURAGED)

  • Reactive must contain an object
  • Reactive cannot contain primitive data types (int, text, etc)
  • Very easy to destroy the state. Refer to link
  • Reactive state can be mutated directly in a function

  • Use Ref Instead (Next Slide)

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

310 of 381

Vue Ref and Mutating State

  • Ref can contain ANY type
  • Ref state can be mutated by accessing .value
  • Without .value, ref returns a ref object
  • Recommended way to deal with Component State in Vue, Reactive has too many limitations

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

311 of 381

Vue Computed and Filtering Data

  • Computed(()=>{ return someExpression; }
  • Computed can be used to handle advanced logic for conditional rendering.
  • Or, it can be used to declartively compute a derived value (eg. likesVueAndStartsWithR)

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

312 of 381

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>

  • Computed can actually fully be omitted in place of using a function but…
  • Computed’s results are cached based on their dependency (eg. if students change)
  • This means that as long as students are the same, multiple calls to likesVueAndStartsWithR will immediately return the result

313 of 381

Vue Complex Conditional Logic

  • Rather than defining conditional logic in the template, a function can be defined
  • This is especially great when multiple complex conditions need to be chained together
  • In this case, doesLikeVueAndStartsWithR returns true or false for a given student if the condition passes
  • However, oftentimes it better to just use computed

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

314 of 381

Vue Watchers and Working With Side Effects

  • Computed used to declaratively compute derived values but…
  • Watch can be used to also perform “side effects” such as mutating the DOM, changing state, performing async operations and etc
  • Watch observes changes in the value, thus removing the necessity for observers!

Example

  • Watch observes changes in question and depending on conditional logic, changes answer by requesting information from an API
  • The side effect performed is mutating answer.value and performing an async operation
  • Watch can be used instead of an observer

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

315 of 381

Vue Deep Watchers

  • Watch can also observe changes in an array or object
  • By default, it only notices if an array/object is created or replaced
  • To allow more fine grained watching (such as mutations) simply add: { deep: true }
  • WARNING: It requires traversing ALL nested properties thus a large performance impact when watching large data structures

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

316 of 381

Vue Lifecycle Hooks - onMounted and onUnmounted

  • onMounted hook can be used to run code after the component has finished the initial render
  • onUnmounted hook can be used to run code after the component stopped being rendered

<script>

import { onMounted, onUnmounted } from "vue";

export default {

setup() {

onMounted(() => {

console.log("My component has mounted");

});

onUnmounted(() => {

console.log("My component has unmounted");

});

},

};

</script>

317 of 381

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 Documentation

318 of 381

Vue’s Composition API vs React’s Hooks

  • Executed once
  • Call-order independent
  • Can be conditional
  • Automatically declares dependencies (in computed and watch)

Dangers

  • None, makes use of Vue’s deep state check and executes once to ensure an intuitive design
  • Executed every time component state changes
  • Call-order sensitive
  • Cannot be conditional
  • Must manually declare dependencies (in useEffect)

Dangers

  • Stale-closure problem → closure (eg. function) captures outdated variable
  • Certain Hooks, such as, useEffect should define state/prop dependency or will execute on ALL state changes
  • Hooks should not be called inside loops, conditions or nested functions

319 of 381

Vue <script setup> - What does it do?

  • Manually exposing state and function is quite time consuming and prone to error, therefore the <script setup> can be used to automate the process
  • It exposes all top level imports and variables
  • It is the recommended way to create Single File Components (SFC)
  • Heavily reduces verbosity of Composition Api

320 of 381

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>

321 of 381

Vue defineProps and defineEmits

  • Props must be defined using defineProps
  • They can either be with an unknown type or with a known type
  • Custom events must be defined using defineEmits
  • To fire a custom event, simply call emit("customEventNameWithParams", params);

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

322 of 381

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>

323 of 381

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>

324 of 381

Vue <script setup> - Limitations

  • “.vue” file format is required
  • Template syntax is required
  • Jsx cannot be used
  • Cannot be used in Composables

325 of 381

For More Vue Information…

  • Select desired API, either Options API or Composition API
  • Documentation will update to desired API

326 of 381

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>

  • <script setup> may not be used!
  • defineComponent is required!
  • lang="jsx" is required!
  • To access state, functions, etc “this.” is required!
  • Refer to Vue templates in the project slide for how to properly handle events and correctly follow MVP
  • Quick Recap:
    • If View uses template syntax and emits event called “eventName”
    • Jsx in presenter sees “onEventName”
    • It appends “on” and converts custom event name to camelCase

// 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>

327 of 381

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>

  • defineComponent can accept props or emits
  • defineComponent has improved type inference

328 of 381

Pinia

Application State store for Vue

329 of 381

Pinia (Or in other words, Vuex 5)

https://pinia.vuejs.org/

  • npm install pinia

// In main.js

import { createPinia } from "pinia";

app.use(createPinia());

330 of 381

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 }

})

331 of 381

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,

};

});

332 of 381

State

// State

const currentPokemon = ref(null);

const currentPokemonDetails = ref(null);

const error = ref(null);

. . .

// remember to return it!

return {

currentPokemon,

currentPokemonDetails,

error,

...

};

  • Application state defined in the store instance
  • For example, the initial value of the currentPokemon is now null
  • Important: It is required to use ref and it is not possible to use reactive

333 of 381

Getters

// Getters or Derived state

const currentPokemonName = computed(() => {

if (currentPokemonDetails.value) {

return currentPokemonDetails.value.name.toUpperCase();

}

return null;

});

// remember to return it!

return {

...

currentPokemonName,

};

  • Mainly used for filtering, returning a value based on the state, etc
  • currentPokemonName returns the pokemon name in all UpperCase

334 of 381

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 {

...

};

  • Actions can be used to change the state
  • Simply change the state the by accessing “.value

335 of 381

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,

...,

};

  • They can also be async
  • And even, an action can call another action

336 of 381

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>

  • In order to watch the state changes of a particular item in the store, one can create a watcher using watch and store the returned function inside a ref
  • watch returns a function that you can call to stop the watcher
  • In order to stop the watcher, you need to only call the function that it returned, in this case unwatchCount.value();
  • { immediate: true } forces the watcher’s callback to be executed immediately

  • Hint: This is a great way of ensuring a good separation of concerns when connecting your Application State to your persistence
  • setPokemonInFirebase passes the entire store to the function, so you can access it’s state or actions

337 of 381

Persisting the Pinia Store

  1. Watch state changes in the store for the relevant states
    1. Save the desired state in Firebase (eg. setPokemonInFirebase(store))

2. Listen to the persistence and dispatch actions!

  • E.g. firebase onValue()
    • "onValue" events on the ref "dinnerModel/numberOfGuests” will dispatch store.setNumberOfGuests(nr)
    • "onValue" events on the ref "dinnerModel/currentDish" will dispatch store.setCurrentDish(dish)

338 of 381

Multiple Stores

  • Best to use more than 1 store!
  • For example, one could have a dinnerStore and userStore
  • You can simply create a store the same as shown in the previous slide in a different file

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 };

});

339 of 381

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 };

});

340 of 381

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}

/>

);

};

},

};

  • In order to access any store, you simply need to import the store that you want to use
  • To access the state / derived state, you simply need to do: store.someAppStateName
  • To access an action, you simply need to do: store.someActionName

341 of 381

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} />

);

};

},

};

  • You can also use Object Destructuring
  • To access the state / derived state, you must use the Utility storeToRefs
  • To access an action, you can do object destructuring as usual

342 of 381

Pinia Flow

  • State : Application State
  • Actions : Used to change the App state
  • View : Appearance. It receives props from Presenter regarding app state. Emits events that Presenter handles and calls correct action

Pinia Store

Presenter

View

Actions

State

Props

Custom Event

343 of 381

Old slides

344 of 381

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 */ } ,

};

345 of 381

“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]

346 of 381

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!

347 of 381

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!

348 of 381

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.

349 of 381

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().

350 of 381

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!).

  • no need to call notifyObservers from each method that changes the Observable: the change can be detected via Proxy, and we can have a single call to notifyObservers() in the proxy handler
  • for multicast notification (one Observable -> more Observers) we still need to manage an array of observers

RxJS is a library that performs advanced operations with observables. Used a lot with Angular.

  • Making an Oservable with RxJSconst observable=new Rx.BehaviorSubject(model);

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

  • Transforming our Observable (Model) to an RxJS Observable:

new rxjs.Observable(function subscribeACB(s){

model.addObserver(function observerACB(){ s.next(model);});

})

Redux store is a multicast Observable (addObserver is called subscribe() ).

351 of 381

“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

352 of 381

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

353 of 381

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:

  1. How to render the UI/component (sometimes in HTML/XML, static UI)
  2. How to update the UI/component in various conditions (in e.g. JavaScript)
    1. Before that: find the component in the tree (via getElementById(), querySelector(), jQuery)

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”)

354 of 381

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:

  • How to render a component (in JavaScript or better JSX)

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)

355 of 381

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)

  1. Framework keeps data (component state) between Component function executions (renderings)
  2. Using e.g. event listener, observer, promise, Component can change its own state
  3. Framework updates (re-renders) the component when its State changes

Up to now we have used a kind of Application State

  1. A state store is kept for the whole application (e.g. DinnerModel, Redux store, Vuex, Recoil)
  2. Using e.g. event listener, promise, Component (or other modules like cloud Persistence) can change application state
  3. Framework (possibly because of notifications from state store like Redux or observer), updates (re-renders) components for which relevant parts of the application state have changed

Frameworks only provide component state and we implement application state using that.

356 of 381

Up to now: pure Components (stateless)

  • Functional
  • Pure functions
    • Deterministic: always render the same given the same props
    • No side-effects

Today: stateful components. They are not pure:

  • rendering depends on state, not just on parameter (props)
  • setting state is a side-effect.

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)

357 of 381

React Component State

(state hook, React state is shallow, React eager to re-render)

358 of 381

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

359 of 381

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.

360 of 381

Vue Component State

(deep state check, selective update)

361 of 381

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

362 of 381

SomeComponent

<SomeComponent prop=val/> or

createElement(SomeComponent, props)

first render

Render

Functional: SomeComponent(props)

Object: render()

Component State changed. Update!

363 of 381

Component Vs Application State

The trend is to move a lot of the state in Application State

  • Often it turns out during development that a piece of state needs to be shared by several components
  • This is one of the use cases that lead to Flux / Redux
  • numberOfGuests, dishes, currentDish used by Summary, Sidebar, Details
    • Search also needs to know about currentDish because it sets it
  • However,
    • searchParams and searchResultsPromiseState only needed by Search
    • currentDishPromiseState only needed by Details
    • Theoretically they could be transformed into Component State. We do for searchParams in TW3
    • Advantage: state stays with the component, which makes it more reusable, publishable etc
    • Disadvantages:
      • framework-dependent promiseResolve and other state work
      • If Search is temporarily removed from UI, its parameters are lost

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

364 of 381

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)

365 of 381

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?

  • Sometimes you don’t need update (e.g. ReactRoot). Only sub-components will update, based on their own component state.
  • If e.g. content (some property or sub-object) of neverChange is modified (!) � you can always force a re-render (update) in React like

const [/*ignored*/, setState]= React.useState();

To force re-render in e.g. an event callback: setState(new Object())

366 of 381

Presenter as Observer (lifecycle motivation example)

An Observer component needs to:

  1. add itself as observer to the Model when the component is created
  2. remove itself as Model observer when the component is taken down
    • (e.g. not displayed any longer by its parent)
      • For example <Show><Summary/></Show> The <Show> may decide not to render its children (i.e. Summary).
      • Or promiseNoData() || <View/> the View will be “taken down” when promiseNoData returns truthy
    • To avoid state change (and thus re-render) of a component that is no longer displayed.
    • To avoid memory leaks (model.observers keeps references to the observers, which have references to the respective components, etc)

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

367 of 381

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

368 of 381

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!

  • either use X, Y (React component state) everywhere
  • or use props.model.X, props.model.Y everywhere!

Recommendation: the props.model form makes Vue presenter code from TW1 and TW2 directly re-usable!

369 of 381

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)}} />

}

  1. Since the state variable name (formerly number) is not used in JSX (props.model.NumberOfGuests is used instead), it can be ignored at destructuring
  2. Calling the observer when adding it allows setting the state without initializing it in React.useState. This is especially useful when the observer copies several state properties from Application State
  3. The returned function can be used directly at its declaration

370 of 381

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();

371 of 381

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")

372 of 381

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

  • nothing (false)
  • or <div class="hidden" ><Child1/><Child2/></div>

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.

373 of 381

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({})})

374 of 381

A few Component State remarks

375 of 381

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)

376 of 381

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));

}

377 of 381

Vuex

Application State store for Vue

378 of 381

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>

379 of 381

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

380 of 381

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>

381 of 381

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"