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