question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Discussion: Improved handling and passing of sources between components

See original GitHub issue

Passing 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:closed
  • Created 7 years ago
  • Reactions:1
  • Comments:12 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
sdebauncommented, Mar 28, 2016

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:

const FooPageComponent = sources => {
  const bar = BarClickableDisplayComponent({...sources,
    item$: sources.someSpecificSourceFromAbove$,
  })

  const baz = BazCreationComponent({...sources,
    isOpen$: bar.click$.map(true),
  })

  const queue$ = baz.item$.map(aLibraryFunctionThatTranslatesItemToQueueTask)

  return {
    queue$,
    DOM: combineLatest(bar.DOM, baz.DOM, (...doms) => div({}, doms)),
  }
}

We are also using the convention that all sources are observables, or functions that return observables, which means things like:

const hello = ListItem({
  title$: just('Hello World'),
})

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. 😃

2reactions
TylorScommented, Mar 28, 2016

I’ve generally chosen to use sources only and to liberally use object-rest-spread, which I’ll still note isn’t a standard JS feature, but is possible via babel plugins.

function Component(sources) {
  const {DOM, HTTP} = sources 
  ...
  const props$ = Observable.just(....)
  const something$ = Observable.just(...)
  const otherComponent({...sources, props$, something$})
}

Read more comments on GitHub >

github_iconTop Results From Across the Web

Facilitating Effective Discussions | Centre for Teaching ...
Students discussing "Initiating and sustaining a lively, productive discussion are among the most challenging activities for an instructor" (Davis, 1993).
Read more >
Nominal Group Technique (NGT) - ASQ
Nominal group technique (NGT) is a structured method for group brainstorming that encourages contributions from everyone. Learn more about NGT at ASQ.org.
Read more >
Learning Objectives - Eberly Center
INSTRUCTIONAL STRATEGIES are chosen to foster student learning towards meeting the objectives. When these components are not aligned, students might rightfully ...
Read more >
Three keys to faster, better decisions - McKinsey
Three practices can help improve decision making and convince skeptical business leaders that there is life after death by committee.
Read more >
How-To Discuss Goals of Care with Patients - PMC - NCBI - NIH
Components of Goals of Care Discussions​​ However, it is important to use these conversations as an opportunity to understand the patient's goals ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found