Discussion: Improved handling and passing of sources between components
See original GitHub issuePassing the sources object around is kind of a “damned-if-you-do, damned-if-you-don’t” scenario. On the one hand, it’s nice when you can use destructuring, like so:
function main({DOM, router}) {
// But now I can't pass sources to child components
}
On the other hand, it’s important to be able to pass sources around:
function Component(sources) {
// ...
}
function main(sources) {
const sinks = Component(sources);
// But now I have lost the convenience of destructuring
}
This brings me to my next point. It’s important to be able to pass adhoc streams of data to child components, with common examples such as props$
or state$
, but then you have to break the standard component contract, function Component(sources)
, by supplying additional arguments, and even then, you still lose the ability to simultaneously destructure and pass sources around. To maintain the standard component function signature/contract, you have to contaminate the sources object with component-specific data such as streams of props, and the component must then take that into account when it passes the sources object to its own children. Should it just pass the contaminated sources along as-is and hope for the best, or should it try to sanitize the sources object before passing it along, and if so, how does it know which sources to keep and which to discard?
I’ve been experimenting with an approach to possibly conquer all of these issues at once, but I’m curious how others feel about the idea. On one hand, it allows for consistent sources-passing, a consistent single-argument component function contract, the ability to destructure, and provides a way to sanitize or customize sources before passing them to children, but on the other hand, it requires any component that makes use of the technique to assume that all of its ancestors (i.e. other component functions already on the stack) to also either opt in, or to at least not butcher the sources object before passing it on.
Here’s what it looks like:
Cycle.run(main, {
// drivers
});
function main(sources) {
return App(upgradeSources(sources)); // Say hello to the `upgradeSources` function
}
function App({ DOM, router, sources }) {
// Note that we get destructuring AND the sources object
function props$ = ... // Make a stream of props for our child component
function state$ = ... // Get some kind of state from somewhere
// Pass along a new sources object, upgraded and augmented for the child component
return SomeComponent(sources.with({ props$, state$ }));
}
function SomeComponent({ DOM, props$, state$, sources }) {
// `sources` is a recursively-nested copy of `arguments[0]`, so that it can be
// passed to local functions
// ... Assign foo$ and bar$ some values
let foo$, bar$; // (Assignment omitted for brevity)
// Provide YetAnotherComponent with foo$ and bar$, but not props$ or state$
const yetAnotherChild = YetAnotherComponent(sources.with({ foo$, bar$ }))
// This one doesn't require any special properties, so create a clean sources
// object, minus extraneous properties (same as calling `sources.with({})`)
const anotherChild = OtherComponent(sources.sanitize());
return // ... return sinks as usual
}
The implementation is as follows:
export function upgradeSources(sources) {
const sourceKeys = Array.from(Object.keys(sources));
const cleanSources = sources => sourceKeys
.reduce((acc, key) => (acc[key] = sources[key], acc), {});
function withCustom(baseSources, customSources) {
const cleanedSources = cleanSources(baseSources);
const nextSources = Object.assign({}, cleanedSources, customSources);
return augment(nextSources, cleanedSources);
}
function augment(nextSources, cleanedSources) {
let augmentedSources = Object.assign({
sanitize: function() {
return augment(cleanSources(this));
},
with: function(custom) {
return withCustom(this, custom);
}
}, nextSources);
return Object.assign(augmentedSources, {sources: augmentedSources});
}
return augment(sources);
}
One might argue that it’s better to just keep everything super simple and not use these sorts of abstractions. A counter-argument would be that the reason people have pain points at all with Cycle is because of a lack of basic, consistent abstractions for situations like these, leading to a lack of clarity about how to handle issues such as those I described in this topic.
I realise there is an issue when it comes to isolation in that the nested sources
property would lose homogeneity with isolated sources, but I’m not going to implement a solution to that without further discussion on the main idea itself.
Thoughts/feedback?
Issue Analytics
- State:
- Created 7 years ago
- Reactions:1
- Comments:12 (6 by maintainers)
Top GitHub Comments
just saw this via productpains, wanted to share our current approach with sparks… though it has been changing on a weekly basis, here’s the kind of stuff we’re doing…
Convention we’re using so far is:
We are also using the convention that all
sources
are observables, or functions that return observables, which means things like:I agree that the lack of transparency in the component args is not a good thing. We’re trying to solve that by keeping our components as small as possible, so you can easily see where sources is being used in your IDE.
We have already started a collection of easily reusable components, and plan on extracting that into a library soon. I want to make sure the naming conventions are standardized amongst all components and fill in a couple of holes (notably a TabControl).
https://github.com/sdebaun/sparks-cyclejs/blob/release/src/components/sdm/ListItem/index.js
PRs, Issues, etc welcome on sparks-cyclejs. 😃
I’ve generally chosen to use
sources
only and to liberally useobject-rest-spread
, which I’ll still note isn’t a standard JS feature, but is possible via babel plugins.