RxJS + Redux + React = Amazing

Post on 16-Apr-2017

6.756 views 7 download

Transcript of RxJS + Redux + React = Amazing

RxJS + Redux + React = Amazing

Jay Phelps | @_jayphelps

Side Effect Management with RxJS

Jay Phelps | @_jayphelps

Managing state stuff is hard

Jay Phelps | @_jayphelps

Redux makes it simple (not necessarily easy)

Jay Phelps | @_jayphelps

Managing async stuff is harder

Jay Phelps | @_jayphelps

Some async is complex regardless of the abstraction

Jay Phelps | @_jayphelps

RxJS makes it manageable

Jay PhelpsSenior Software Engineer |

@_jayphelps

What is redux?

Jay Phelps | @_jayphelps

Crash Course

Jay Phelps | @_jayphelps

What is redux?

Jay Phelps | @_jayphelps

Provides predicable state management using actions and reducers

What's an "action"?

Jay Phelps | @_jayphelps

Describes something has (or should) happen, but they don't specify how it should be done

Jay Phelps | @_jayphelps

{type:'CREATE_TODO',payload:'BuildmyfirstReduxapp'}

What's an "reducer"?

Jay Phelps | @_jayphelps

A pure function that takes the previous state and an action and returns the new state

What's an "reducer"?

Jay Phelps | @_jayphelps

Sometimes it returns the previous state

(state,action)=>state

What's an "reducer"?

Jay Phelps | @_jayphelps

Sometimes it computes new state

(state,action)=>state+action.payload

Jay Phelps | @_jayphelps

constcounter=(state=0,action)=>{switch(action.type){case'INCREMENT':returnstate+1;

case'DECREMENT':returnstate-1;default:returnstate;}};

Jay Phelps | @_jayphelps

Reducers handle state transitions, but they must be done synchronously.

Jay Phelps | @_jayphelps

constcounter=(state=0,action)=>{switch(action.type){case'INCREMENT':returnstate+1;

case'DECREMENT':returnstate-1;default:returnstate;}};

Jay Phelps | @_jayphelps

What are async stuff do we commonly do?

Async

Jay Phelps | @_jayphelps

• User interactions (mouse, keyboard, etc) • AJAX • Timers/Animations • Web Sockets • Work Workers, etc

Jay Phelps | @_jayphelps

Some can be handled synchronously

Jay Phelps | @_jayphelps

<buttononClick={()=>dispatch({type:'INCREMENT'})}>Increment</button>

Jay Phelps | @_jayphelps

constcounter=(state=0,action)=>{switch(action.type){case'INCREMENT':returnstate+1;

case'DECREMENT':returnstate-1;default:returnstate;}};

Jay Phelps | @_jayphelps

Sometimes you need more control

Jay Phelps | @_jayphelps

• AJAX cancellation/composing • Debounce/throttle/buffer/etc • Drag and Drop • Web Sockets, Work Workers, etc

Jay Phelps | @_jayphelps

Use middleware to manage async / side effects

Jay Phelps | @_jayphelps

Most of them use callbacks or Promises

Callbacks

Jay Phelps | @_jayphelps

The most primitive way to handle async in JavaScript

Callbacks

Jay Phelps | @_jayphelps

fetchSomeData((error,data)=>{if(!error){dispatch({type:'HERES_THE_DATA',data});}});

Callback Hell

Jay Phelps | @_jayphelps

fetchSomeData(id,(error,data)=>{if(!error){dispatch({type:'HERES_THE_FIRST_CALL_DATA',data});

fetchSomeData(data.parentId,(error,data)=>{if(!error){dispatch({type:'HERES_SOME_MORE',data});fetchSomeData(data.parentId,(error,data)=>{if(!error){dispatch({type:'OMG_MAKE_IT_STOP',data});}});}});}});

Promises

Jay Phelps | @_jayphelps

fetchSomeData(id).then(data=>{dispatch({type:'HERES_THE_FIRST_CALL_DATA',data});returnfetchSomeData(data.parentId);}).then(data=>{dispatch({type:'HERES_SOME_MORE',data});returnfetchSomeData(data.parentId);}).then(data=>{dispatch({type:'OKAY_IM_DONE',data});});

Promises

Jay Phelps | @_jayphelps

• Guaranteed future • Immutable • Single value • Caching

Promises

Jay Phelps | @_jayphelps

• Guaranteed future • Immutable • Single value • Caching

Promises

Jay Phelps | @_jayphelps

• Guaranteed future • Immutable • Single value • Caching

Jay Phelps | @_jayphelps

Promises cannot be cancelled

Jay Phelps | @_jayphelps

Why would you want to cancel?

Jay Phelps | @_jayphelps

• Changing routes/views • Auto-complete • User wants you to

Jay Phelps | @_jayphelps

• Changing routes/views • Auto-complete • User wants you to

Jay Phelps | @_jayphelps

Daredevil

Jay Phelps | @_jayphelps

Daredevil

TheGetDown

Here’sDaredevil!

Jay Phelps | @_jayphelps

Daredevil

Jay Phelps | @_jayphelps

Daredevil

TheGetDown

Jay Phelps | @_jayphelps

Cancelling is common and often overlooked

Promises

Jay Phelps | @_jayphelps

• Guaranteed future • Immutable • Single value • Caching

Only AJAX is single value

Jay Phelps | @_jayphelps

• User interactions (mouse, keyboard, etc • AJAX • Animations • WebSockets, Workers, etc

Jay Phelps | @_jayphelps

What do we use?

Jay Phelps | @_jayphelps

Observables

Observables

Jay Phelps | @_jayphelps

• Stream of zero, one, or more values • Over any amount of time • Cancellable

Jay Phelps | @_jayphelps

Streams are a set, with a dimension of time

Jay Phelps | @_jayphelps

Being standardized for ECMAScript aka JavaScript

RxJS

Jay Phelps | @_jayphelps

“lodash for async” - Ben Lesh

Crash Course

Jay Phelps | @_jayphelps

Creating Observables

Jay Phelps | @_jayphelps

• of('hello')• from([1,2,3,4])• interval(1000)• ajax('http://example.com')• webSocket('ws://echo.websocket.com')• fromEvent(button,‘click')• many more…

Subscribing

Jay Phelps | @_jayphelps

myObservable.subscribe(value=>console.log('next',value));

Subscribing

Jay Phelps | @_jayphelps

myObservable.subscribe(value=>console.log('next',value),err=>console.error('error',err));

Subscribing

Jay Phelps | @_jayphelps

myObservable.subscribe(value=>console.log('next',value),err=>console.error('error',err),()=>console.info('complete!'));

Observables can be transformed

Jay Phelps | @_jayphelps

map, filter, reduce

Observables can be combined

Jay Phelps | @_jayphelps

concat, merge, zip

Observables represent time

Jay Phelps | @_jayphelps

debounce, throttle, buffer, combineLatest

Observables are lazy

Jay Phelps | @_jayphelps

retry, repeat

Jay Phelps | @_jayphelps

Observables can represent just about anything

Jay Phelps | @_jayphelps

Let’s combine RxJS and Redux!

Jay Phelps | @_jayphelps

Side effect management for redux, using Epics

What is an Epic?

Jay Phelps | @_jayphelps

A function that takes a stream of all actions dispatched and returns a stream of new actions to dispatch

Jay Phelps | @_jayphelps

“actions in, actions out”

//Thisispseudocode,notrealfunctionpingPong(action,store){if(action.type==='PING'){return{type:'PONG'};}}

Jay Phelps | @_jayphelps

Sort of like this

functionpingPongEpic(action$,store){returnaction$.ofType('PING').map(action=>({type:'PONG'}));}

An Epic

Jay Phelps | @_jayphelps

constpingPongEpic=(action$,store)=>action$.ofType('PING').map(action=>({type:'PONG'}));

An Epic

Jay Phelps | @_jayphelps

An Epic

Jay Phelps | @_jayphelps

constpingPongEpic=(action$,store)=>action$.ofType('PING').delay(1000)//<—that'sit.map(action=>({type:'PONG'}));

Jay Phelps | @_jayphelps

constisPinging=(state=false,action)=>{switch(action.type){case'PING':returntrue;

case'PONG':returnfalse;

default:returnstate;}};

constpingPongEpic=(action$,store)=>action$.ofType('PING').delay(1000).map(action=>({type:'PONG'}));

Jay Phelps | @_jayphelps

Jay Phelps | @_jayphelps

Debounced increment / decrement button

Jay Phelps | @_jayphelps

constcounter=(state=0,action)=>{switch(action.type){case'INCREMENT':returnstate+1;

case'DECREMENT':returnstate-1;default:returnstate;}};

Jay Phelps | @_jayphelps

constincrementEpic=(action$,store)=>action$.ofType('INCREMENT_DEBOUNCED').debounceTime(1000).map(()=>({type:'INCREMENT'}));

constdecrementEpic=(action$,store)=>action$.ofType('DECREMENT_DEBOUNCED').debounceTime(1000).map(()=>({type:'DECREMENT'}));

Jay Phelps | @_jayphelps

constincrementEpic=(action$,store)=>action$.ofType('INCREMENT_DEBOUNCED').debounceTime(1000).map(()=>({type:'INCREMENT'}));

constdecrementEpic=(action$,store)=>action$.ofType('DECREMENT_DEBOUNCED').debounceTime(1000).map(()=>({type:'DECREMENT'}));

Jay Phelps | @_jayphelps

Those are contrived examples, obviously

Jay Phelps | @_jayphelps

Warning: non-trivial examples ahead, don’t struggle to read them entirely

Jay Phelps | @_jayphelps

Auto-complete

Jay Phelps | @_jayphelps

onKeyUp(e){const{store}=this.props;const{value}=e.target.value;

if(this.queryId){clearTimeout(this.queryId);}

this.queryId=setTimeout(()=>{if(this.xhr){this.xhr.abort();}

constxhr=this.xhr=newXMLHttpRequest();xhr.open('GET','https://api.github.com/search/users?q='+value);xhr.onload=()=>{if(xhr.status===200){store.dispatch({type:'QUERY_FULFILLED',payload:JSON.parse(xhr.response).items});}else{store.dispatch({type:'QUERY_REJECTED',error:true,payload:{message:xhr.response,status:xhr.status}});}};xhr.send();},500);}

Plain JS

Jay Phelps | @_jayphelps

Epic

constautoCompleteEpic=(action$,store)=>action$.ofType('QUERY').debounceTime(500).switchMap(action=>ajax('https://api.github.com/search/users?q='+value).map(payload=>({type:'QUERY_FULFILLED',payload})));

Jay Phelps | @_jayphelps

EpicconstautoCompleteEpic=(action$,store)=>action$.ofType('QUERY').debounceTime(500).switchMap(action=>ajax('https://api.github.com/search/users?q='+value).map(payload=>({type:'QUERY_FULFILLED',payload})).catch(payload=>[{type:'QUERY_REJECTED',error:true,payload}]));

Jay Phelps | @_jayphelps

EpicconstautoCompleteEpic=(action$,store)=>action$.ofType('QUERY').debounceTime(500).switchMap(action=>ajax('https://api.github.com/search/users?q='+value).map(payload=>({type:'QUERY_FULFILLED',payload})).takeUntil(action$.ofType('CANCEL_QUERY')).catch(payload=>[{type:'QUERY_REJECTED',error:true,payload}]));

Jay Phelps | @_jayphelps

OK, show me really non-trivial examples

Jay Phelps | @_jayphelps

Bidirectional, multiplexed Web Sockets

Jay Phelps | @_jayphelps

classExample{@autobindcheckChange(e){const{value:key,checked}=e.target;this.subs=this.subs||[];

if(checked){consthandler=e=>{constdata=JSON.parse(e.data);if(data.key===key){this.updateValue(key,data.value);}};

this.subs.push({key,handler})

constsocket=this.getSocket(()=>{this.setState({socketOpen:true});this.subs.forEach(({key})=>socket.send(JSON.stringify({type:'sub',key})));});

socket.addEventListener('message',handler);}else{constindex=this.subs.findIndex(x=>x.key===key);if(index!==-1){this.subs.splice(index,1);}

const{socket}=this;if(socket&&socket.readyState===1){socket.send(JSON.stringify({type:'unsub',key}));this.setInactive(key)lif(this.subs.length===0){socket.close();}}}}

componentWillUnMount(){if(this.socket&&this.socket.readyState===1){this.socket.close();}}

Plain JS

getSocket(callback){const{socket}=this;if(socket&&socket.readyState===1){setTimeot(callback);}else{if(this.reconnectId){clearTimeout(this.reconnectId);}

socket=this.socket=newWebSocket(‘ws://localhost:3000');socket.onopen=()=>{callback();};socket.onerror=()=>{this.reconnectId=setTimeout(()=>this.getSocket(callback),1000);this.setState({socketOpen:false});};socket.onclose=(e)=>{if(!e.wasClean){this.reconnectId=setTimeout(()=>this.getSocket(callback),1000);}this.setState({socketOpen:false});};}returnsocket;}}

Jay Phelps | @_jayphelps

Too much code

Jay Phelps | @_jayphelps

As an Epicconstsocket=WebSocketSubject.create('ws://stock/endpoint');

conststockTickerEpic=(action$,store)=>action$.ofType('START_TICKER_STREAM').mergeMap(action=>socket.multiplex(()=>({sub:action.ticker}),()=>({unsub:action.ticker}),msg=>msg.ticker===action.ticker).retryWhen(err=>window.navigator.onLine?Observable.timer(1000):Observable.fromEvent(window,'online')).takeUntil(action$.ofType('CLOSE_TICKER_STREAM').filter(closeAction=>closeAction.ticker===action.ticker)).map(tick=>({type:'TICKER_TICK',tick})));

redux-observable

Jay Phelps | @_jayphelps

• Makes it easier to compose and control complex async tasks, over any amount of time

• You don't need to manage your own Rx subscriptions

• You can use redux tooling

But…

Jay Phelps | @_jayphelps

Jay Phelps | @_jayphelps

You should probably know redux and RxJS in advance

Jay Phelps | @_jayphelps

RxJS has a bit of a learning curve

Jay Phelps | @_jayphelps

“Reactive Programming”

Co-author

Jay Phelps | @_jayphelps

Ben LeshSenior UI Engineer |

@benlesh

Jay Phelps | @_jayphelps

https://redux-observable.js.org

Thanks!

@_jayphelps