The Object⏩to⏩Stream mindset shift

The Object⏩to⏩Stream mindset shift

Source: Dev.to

Why objects feel comfortable ## How are streams different ## OOP vs SP, side by side ## Example 2: Live search ## Example 3: Form validation ## Where to go from here ## Learn More If you took programming lessons, chances are they taught you classes, inheritance, encapsulation — the usual stuff of Object Oriented Programming (OOP). Perhaps it made even sense: wrap data and behaviour together, send messages between instances and build a system that models a crystal-clear vision the world. But what happens when we move away from the world of things and into the world of workflows? In the real world, everything changes, all the time, right? That’s where Stream Oriented Programming (SP) comes in. Instead of thinking of "objects holding state," we think in terms of "information/data/values flowing through time". And if you’ve worked with RxJS before, you already know that streams let you model async data in a very natural way. The next step is to stop bolting them onto OOP frameworks and instead build the UI itself as a graph of streams. Let’s look at a very familiar OOP pattern. Imagine a simple counter: This is straightforward: the Counter object holds state, has methods, and updates the DOM whenever something changes. Now let’s look at SP, using Rimmel.js: Here’s the mental shift: No explicit render() call, no mutable this.count. The button is “subscribed” to the count stream, and the DOM updates automatically whenever count changes. In OOP, a live search usually means: In SP with Rimmel.js, the logic is expressed declaratively: Here, keystrokes are a stream. Debouncing is just an operator. Fetching is a transformation. Rendering the results is another subscription. No manual cleanup, no "mutable query field". SP approach with Rimmel.js: Validation logic, enabled/disabled state is just a composition of streams. If you’re curious, Rimmel.js is a great playground to explore this mindset further. It’s lightweight, RxJS-friendly, and built around streams from the ground up. 👉 Try it out here: Rimmel.js on GitHub And next time you’re about to spin up a class with a render() method, ask yourself: what would this look like if it were a stream? Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: class Counter { private count = 0; increment() { this.count++; this.render(); } render() { return `<button>Click me (${this.count})</button>`; } } const counter = new Counter(); document.body.innerHTML = counter.render(); document.body.addEventListener('click', () => counter.increment()); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: class Counter { private count = 0; increment() { this.count++; this.render(); } render() { return `<button>Click me (${this.count})</button>`; } } const counter = new Counter(); document.body.innerHTML = counter.render(); document.body.addEventListener('click', () => counter.increment()); COMMAND_BLOCK: class Counter { private count = 0; increment() { this.count++; this.render(); } render() { return `<button>Click me (${this.count})</button>`; } } const counter = new Counter(); document.body.innerHTML = counter.render(); document.body.addEventListener('click', () => counter.increment()); COMMAND_BLOCK: import { BehaviorSubject, scan } from 'rxjs'; import { rml } from 'rimmel'; const count = new BehaviorSubject(0).pipe( scan(x => x + 1) ); const App = () => rml` <button onclick="${count}"> Click me (${count}) </button> `; document.body.innerHTML = App(); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { BehaviorSubject, scan } from 'rxjs'; import { rml } from 'rimmel'; const count = new BehaviorSubject(0).pipe( scan(x => x + 1) ); const App = () => rml` <button onclick="${count}"> Click me (${count}) </button> `; document.body.innerHTML = App(); COMMAND_BLOCK: import { BehaviorSubject, scan } from 'rxjs'; import { rml } from 'rimmel'; const count = new BehaviorSubject(0).pipe( scan(x => x + 1) ); const App = () => rml` <button onclick="${count}"> Click me (${count}) </button> `; document.body.innerHTML = App(); COMMAND_BLOCK: const item = str => rml`<li>${str}</li>`; const typeahead = new Subject<string>().pipe( debounceTime(300), switchMap(q => ajax.getJSON(`/countries?q=${q}`)), map(list => list.map(item).join('')) ); const App = () => rml` <h1>Basic Typeahead example</h1> <input placeholder="Search countries..." oninput="${Value(typeahead)}"> <ul> ${typeahead} </ul> `; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const item = str => rml`<li>${str}</li>`; const typeahead = new Subject<string>().pipe( debounceTime(300), switchMap(q => ajax.getJSON(`/countries?q=${q}`)), map(list => list.map(item).join('')) ); const App = () => rml` <h1>Basic Typeahead example</h1> <input placeholder="Search countries..." oninput="${Value(typeahead)}"> <ul> ${typeahead} </ul> `; COMMAND_BLOCK: const item = str => rml`<li>${str}</li>`; const typeahead = new Subject<string>().pipe( debounceTime(300), switchMap(q => ajax.getJSON(`/countries?q=${q}`)), map(list => list.map(item).join('')) ); const App = () => rml` <h1>Basic Typeahead example</h1> <input placeholder="Search countries..." oninput="${Value(typeahead)}"> <ul> ${typeahead} </ul> `; COMMAND_BLOCK: const App = () => rml` const submit = new Subject<FormData>; const valid = submit.pipe( map(({ email, password }) => ALL( email.includes('@'), password.length >= 6 )) ); const invalid = valid.pipe( map(x=>!x) ); return rml` <form onsubmit="${AsFormData(submit)}"> <input name="email"> <input name="password" type="password"> <button disabled="${invalid}">Submit</button> </div> `; } document.body.innerHTML = App(); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const App = () => rml` const submit = new Subject<FormData>; const valid = submit.pipe( map(({ email, password }) => ALL( email.includes('@'), password.length >= 6 )) ); const invalid = valid.pipe( map(x=>!x) ); return rml` <form onsubmit="${AsFormData(submit)}"> <input name="email"> <input name="password" type="password"> <button disabled="${invalid}">Submit</button> </div> `; } document.body.innerHTML = App(); COMMAND_BLOCK: const App = () => rml` const submit = new Subject<FormData>; const valid = submit.pipe( map(({ email, password }) => ALL( email.includes('@'), password.length >= 6 )) ); const invalid = valid.pipe( map(x=>!x) ); return rml` <form onsubmit="${AsFormData(submit)}"> <input name="email"> <input name="password" type="password"> <button disabled="${invalid}">Submit</button> </div> `; } document.body.innerHTML = App(); - I have an object. - The object has state. - The object has behaviour. - When behaviour is triggered, the object mutates itself, or in worst cases, the rest of the world. - I don’t have an object. - I have a stream. - That stream represents state over time. - Events are also streams, merged into the same flow. - Rendering isn’t a method call — it’s just describing how streams connect. - Grab an input field. - Add a keyup listener. - Cancel pending timers manually. - Fire off fetch requests. - Update a result list. - Each input is tied to a property. - Each property has flags like isValid. - Validation is run imperatively on every change. - A form object aggregates results.