How Reactive Programming simplifies complicated Frontend applications
Anna Shavurska, Viseven
UI becomes more complicated
A move to Reactiveness
Reactive Code
let a = 1;
let b = 2;
let sum = a + b;
console.log(sum); // 3
a = 2;
console.log(sum); // ? 4
What we're doing
eWizard
- A content management platform
- Top pharma companies: Bayer, Novartis
- > 50 countries
- Emails, Presentations, Sites, Surveys
What reactive programming is...
Stream - is like an array in time
Stream Example
button.addEventListener('click', event => {
console.log(event);
});
Streams:
- variables,
- user inputs,
- properties,
- events, etc.
Pull -> Push
Iterator -> Observer
const hi = 'hi';
const iterator = hi[Symbol.iterator]();
iterator.next(); // 'h'
iterator.next(); // 'i'
iterator.next(); // undefined
import { from } from 'rxjs';
const observable = from('hi');
observable.subscribe((x) => {
console.log(x);
});
// 'h', 'i'
Rx - Reactive
eXtensions
Simple Example
button.addEventListener('click', event => {
console.log(event);
});
fromEvent(button, 'click')
.subscribe(event => console.log(event));
Pipes
Operators are Pipeable
const example = sourceOne.pipe(
concat(sourceTwo),
filter(num => num % 2 === 0)
);
example.subscribe(val => console.log(val));
But WTH I need this Reactive stuff?
Let’s think about writing complex SPA
Managing state stuff is hard
Vuex makes it simple
(not necessarily easy)*
It doesn’t do anything to help you manage async code.
Frontend Development: mostly synchronous or asynchronous?
We tend to think synchronously. We write code in blocks that are read top to bottom, left to right. If this, then this, else this…
But the truth is:
What are we doing with code?
- Handling user input, DOM events
- Animations
- AJAX/JSONP
- Web Sockets/SSE
- Updating DOM
Managing async stuff is harder
How Rx helps to manage async stuff?
- Rx operators are awesome$
- Observables are cancelable
Drag-n-drop with optimization
Steps:
- Listen mouse events
- Transform mouse events to drag events
- Take a draggeable element on dragstart
- And create a preview element for it
- Move both: preview element and draggeable element
- Remove preview element on dragend
Create Streams from Mouse Events
const mouseDown$ = fromEvent(body, 'mousedown')
.pipe(map(getCoordsFromEvent));
const mouseMove$ = fromEvent(body, 'mousemove')
.pipe(map(getCoordsFromEvent));
const mouseUp$ = fromEvent(body, 'mouseup')
.pipe(map(getCoordsFromEvent));
And transform them to Drag Events
const dragStart$ = mouseDown$
.pipe(flatMap(() => mouseMove$.pipe(takeUntil(mouseup$),take(1))));
const dragMove$ = mousedown$
.pipe(flatMap(() => mouseMove$.pipe(takeUntil(mouseup$))));
const dragEnd$ = dragStart$
.pipe(flatMap(() => mouseUp$.pipe(take(1))));
Then take a dragged element
const dragElement$ = dragStart$
.pipe(
map(({ x, y }) => document.elementFromPoint(x, y)),
tap(dragEl => dragEl.classList.add('active')),
share(),
);
And create a preview element
const previewElement$ = dragStart$
.pipe(
withLatestFrom(dragElement$),
map(([{ x, y }, dragEl]) => createPreviewElement(dragEl, x, y)),
tap(previewElement => body.appendChild(previewElement)),
share(),
);
withLatestFrom
Now move a preview element
const movePreviewElement$ = dragMove$
.pipe(
withLatestFrom(previewElement$),
tap(([{ x, y }, previewEl]) => {
movePreviewElement(previewEl, x, y))
}),
);
And move dragged element itself
const moveDragElement$ = dragMove$
.pipe(
withLatestFrom(dragElement$),
tap(([{ x, y }, dragEl]) => moveDragElement(dragEl, x, y)),
);
sampleTime
Optimize element movement
const moveDragElement$ = dragMove$
.pipe(
withLatestFrom(dragElement$),
sampleTime(200),
tap(([{ x, y }, dragEl]) => moveDragElement(dragEl, x, y)),
);
Then drop the element
const finishMovement$ = dragEnd$
.pipe(
withLatestFrom(previewElement$, dragElement$),
tap(([_, previewEl, dragEl]) => {
previewEl.remove();
dragEl.classList.remove('active');
}),
);
Don't forget to subscibe
movePreviewElement$.subscribe(() => {});
moveDragElement$.subscribe(() => {});
finishMovement$.subscribe(() => {});
That's it! So simple
Vue.js + Rx.js
Save Example
vue-rx
RxJS integration for Vue.js.
Handles subscription/unsubscription for you.
Subscriptions
export default {
name: 'save-changes',
subscriptions: {
statusMessage: new Observable(...)
}
};
//bind to it normally in templates
<p>{{ msg }}</p>
v-stream
<v-btn v-stream:click="save$">Save</v-btn>
export default {
domStreams: ['save$'],
subscriptions () {
return {
statusMessage: this.save$.pipe(map(() => 'Saving'))
}
}
};
Vuex Store
const store = {
state: { status: Status.Initial }
mutations: {
setStatus(state, { status }) { state.status = status; }
},
actions: {
save({ commit }) {
commit('setStatus', { status: Status.Saving });
saver.save();
},
setStatus({ dispatch, commit }, data) {
commit('setStatus', data);
},
},
};
How to use Vue-rx + Vuex?
Vue-rx - Vuex communication
Dispatch Action
methods: {
...mapActions('saveModule', ['save'])
}
export default {
subscriptions () {
const status$ = this.$watchAsObservable('status',{immediate:true})
.pipe(
pluck('newValue'),
share()
);
return {
statusMessage: status$.pipe(map(this.getStatusMessages)),
}
},
computed: {
...mapState('saveModule', ['status']),
}
};
In what part of
Vue app
Rx may be
used?
How to start?
OMG Rx.js is so confusing!
Stop worrying about the operators
Seriously!
Observables are not scarier than promises
promise.then(resolveFn);
observable.subscribe(nextFn);
Then try to use 'map' to chain observables.
What Operator Do I Use???
Remain calm, it's ok
Use operators that you know
DO NOT Rx All the things
You can build your app as one big observable...
but please don't
Use Rx where it's best suited
- Composing multiple events together
- Adding delay
- Clientside backpressure
- Coordination async tasks
- When cancellation is required