a framework agnostic approach
BATMAN.JS
a Frontend Application?
never best practices
aka the »traditional website«
Server
https://www.awesome-site.io
request page
Server
https://www.awesome-site.io
request page
respond with Markup / CSS / JS / ...
Server
https://www.awesome-site.io/navigate/to/other/url
request another url
Server
https://www.awesome-site.io/navigate/to/other/url
request another url
respond with Markup / CSS / JS / ...
aka »Web-App«
Server
https://www.awesome-site.io
request page
Server
https://www.awesome-site.io
respond with Markup / CSS / JS / ...
request page
Server
https://www.awesome-site.io/#/navigate/to/other/url
request data
Server
https://www.awesome-site.io/#/navigate/to/other/url
respond with JSON
request data
Authentication
Loading the initial page (resources)
Providing data
of a »Single Page App«
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
<!-- entry point -->
<div id="app"/>
<script src="/path/to/bundle.js"></script>
</body>
</html>
Routing
Application Logic (Structure)
Rendering
Data Layer
Asynchronicity
Tooling
entry point
entry point
entry point
entry point
https://www.awesome-site.io/#/navigate/to/other/url
#
window.addEventListener('hashchange', () => {
// route changed, handle it!
});
#
HTML5 History API (without #)
window.onpopstate = function(event) {
// route changed, handle it!
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
HTML
function() {}
function() {}
function() {}
JSON
Model => View
Model <=> View
JSON
HTML
function() {}
JSON
function() {}
reason about your code
const result = askForData();
this.state = result.X
FireAndForget();
- intention to do something
- simple description (string)
- optional: payload
- receives actions
- dispatches actions to stores
- single source of truth
- updates state when action appears
- fires event when state changes
- triggers actions
- acts on change event from store
»Redux is a predictable state container for JavaScript apps.«
ACTION
STORE
Reducers
ACTION
STORE
Reducers
The only way to mutate state is to emit an action, an object describing what happened
ACTION
STORE
Reducers
ACTION
STORE
Reducers
To specify how the state tree is transformed by actions, you write pure reducers
const todos = (state = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
{
id: state.length + 1,
text: action.text,
completed: false
}
];
default:
return state;
}
};
const todoApp = combineReducers({todos});
const store = createStore(todoApp);
store.subscribe(() => {
console.log(store.getState());
});
store.dispatch({
type: 'ADD_TODO',
text: 'Mett kaufen'
});
store.dispatch({
type: 'ADD_TODO',
text: 'Mate trinken'
});
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({}); // initial state
return { getState, dispatch, subscribe};
};
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
}, {});
};
};
[ 14, 9, 5, 2, 10, 13, 4 ]
aka »Observable«
(detect the number of button clicks)
// Make the raw clicks stream
var button = document.querySelector('.this');
var clickStream = Rx.Observable.fromEvent(button, 'click');
// HERE
// The 4 lines of code that make the multi-click logic
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
// Same as above, but detects single clicks
var singleClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x === 1; });
// Listen to both streams and render the text label accordingly
singleClickStream.subscribe(function (event) {
document.querySelector('h2').textContent = 'click';
});
multiClickStream.subscribe(function (numclicks) {
document.querySelector('h2').textContent = ''+numclicks+'x click';
});
Rx.Observable.merge(singleClickStream, multiClickStream)
.throttle(1000)
.subscribe(function (suggestion) {
document.querySelector('h2').textContent = '';
});
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
<!-- entry point -->
<div id="app"/>
<script src="/path/to/bundle.js"></script>
</body>
</html>
f(json) => view
»I have no idea what I should re-render. You figure it out.«
»I know exactly what changed and what should be re-rendered because I control your models and views.«
»I have no idea what changed, so I'll just check everything that may need updating.«
»I have no idea what changed so I'll just re-render everything and see what's different now.«
Server
Data-Layer
HTTP
var request = new XMLHttpRequest();
request.open('GET', '/url/to/get/json', true);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
console.log(data);
}
};
request.send();
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
console.log(data);
});
fetch('https://api/url/to/get/json').then(function(response) {
return response.json();
}).then(function(jsonResponse) {
console.log(jsonResponse);
});
Local Storage
WebSockets
It's the nature of the web
asyncFunction(function (response) {
// do something
});
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
// do something
});
});
});
});
});
});
});
});
});
});
const promise = new Promise((resolve, reject) => {
if (/* success */) {
resolve("success");
}
else {
reject(Error("error"));
}
});
promise.then((result) => {
console.log(result); // "success"
}, (err) {
console.log(err); // Error: "error"
});
var clickStream = Rx.Observable.fromEvent(button, 'click');
// HERE
// The 4 lines of code that make the multi-click logic
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
Let's code your own SPA
from scratch
Routing
Application Logic (Structure)
Rendering
Data Layer
Asynchronicity
Tooling
Data Layer
MVVM
Two-Way Data-Binding
#
Promises
Fetch API
BATMAN.JS
Resilience - Jeremy Keith @adactio (Beyond Tellerrand 2016)
Edsger W. Dijkstra