Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global sources & sinks #191

Closed
Cmdv opened this issue Dec 6, 2015 · 28 comments
Closed

Global sources & sinks #191

Cmdv opened this issue Dec 6, 2015 · 28 comments

Comments

@Cmdv
Copy link
Contributor

Cmdv commented Dec 6, 2015

How do you pass sinks back up a nested tree without piping it through each branch?

eg: 3 pages, on one of the pages there is intent to +1 / -1 to a number. I want all other pages to be able to listen to that change and update their own number. if I start with 0 click + 1 and move around the app all pages have a number 1.

correct me if I'm wrong but it feels like that sort of state is only kept within each component? so your sources at the Cycle.run level don't have a clue about whats going on elsewhere unless you passed it along each level until it reaches the top. Not so bad one level deep but what about 3-4 levels deep!

redux is a little noisy but it is passing everything back to the top level and feeding it down when a change happens, then the whole tree has the ability to listen to that change (in React via it's props)

Here is a link get a visual representation of what I'm trying to achieve.

@Cmdv Cmdv changed the title Global sources $ sinks Global sources & sinks Dec 6, 2015
@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

What’s wrong with returning state$ from components? To me, it makes completely sense that a parent component carries the responsibility of returning its state$, possibly composed of state$ returned from its children, to its own parent.

There’s another pattern you can use, and that is to inject components into other components. This way, you can instantiate a component on an upper level, where you then have access to its state$, and then pass the component to other components that may be responsible of only the rendering.

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

where would you're single point of truth live in this pattern? just sounds like state$ is being passed all around and you have to work on pipping it through out the app.

Having that single point which you can refer to anywhere in the app is a really useful tool, when you start dealing with larger apps.

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

Aren’t component state determined by the properties they receive? How then can you have a single point of truth unless you gather it from multiple sources?

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

This is what I'm proposing a way to channel state into a global state object.

const sources = {
  State: {store} // store would be where all global state lives
  DOM: makeDOMDriver('#application'),
  History: makeHistoryDriver({
    hash: false,
    queries: true,
  }),
};
run(mainApp,sources);

The only problem that I can't work out is in children component you would need the ability to update store Driver, so different parts of the app who are also using that store would get updated on change.
This does sound like a driver of sorts, a global state driver.

or do you think that it would have no value and you'd be quite happy without it?

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

I know this isn't going to be liked but the way React has props which automatically bubbles down a tree is really useful. In Cycle you would have to manually pass down that information from parent to child.

Redux took advantage of that channel and used it to pipe props.store which can be called from any child element. The only bit that is scruffy in Redux is the way they push the changes back up. I think this could possibly be done with a single driver in Cycle and the state pushing could be handled by the driver.

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

You are proposing a storage driver of some sort. I have no problem with that. Actually, we already have a LocalStorage driver.

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

cool, just to expand a little on the idea of bubbling $state down.

img_5273

The black lines are how state would travel around the app via a state driver. the blue ones would be how to do it currently.

This next image is if 2 wanted to send a message back to 1

img_5274

Now the bit that I can't work out is you would still have to manually pass $state down from each parent => child. If you are doing that then you might as well be making the connection back up?

Do people think there would be a way for this information to be automatically passed down via it's responses? maybe a custom wrapper for all components that has this built in?!? Or is that just adding/imposing too much.

Cycle is really clean at the core but it feels like a sore point to be having to manually make sure the $state pipping is correct. You still then have to have a mental map to make sure you are passing things up/down each branch.

With React I know that if a prop is declare say 3 levels up on a parent, it can still be called from the bottom child without having to manually pass it through each parent => child.

Is this just a bad idea?

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

Personally, I don’t like the magic that you are proposing. I think it defeats the purpose of reasoning about data flow. How components are composed should be entirely up to the developer. What state of a child is relevant for a parent should also be entirely up to the developer. How the data flows should be entirely up to the developer. But feel free to bypass all that and implement your app as you like. However, I wouldn’t recommend such an app as a good starting point for new Cycle.js developers.

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

Your data flow is going to be one massive mess though, if you're implying that everything should be manually piped. That idea is fine when you're dealing with something a couple of levels deep but going by my pictures say I now want page F to have this $state I'm going to have to do a load more pipping make sure each child => parent relationship is correct.

This is why I feel a single point of truth really comes in handy when dealing with a lot of shared state through multiple levels of parent => child relationships.

because if you're wanted these action -> reaction to happen on 5 pages in the app but not the others, that is going to take some pipping without some form of automatic bubbling.

Yeah you have a good reasoning about your app's data flow but lets now add another 3 action -> reaction all going to different pages, ok lets start piping them up and down. I don't think you're data flow is going to be as much fun anymore or have as much clarity that you feel you'd get from pipping everything manually.

@staltz
Copy link
Member

staltz commented Dec 6, 2015

@Cmdv this is just the difference between top-down approach and bottom-up approach.

If you want global shared state like in Flux and Redux, then you really actually just want one dataflow component, because the Cycle.js equivalent of props-only View in Flux is just functions view : state -> vtree.

Single global state object is the top-down approach where you start with the assumption that all state is in that global object, and all the views are just derivations of that.

Stateful components is the bottom-up approach where you start with existing components and you put them together by composing their behaviors. In this approach, manual "piping" as you call is unavoidable. To some extent in my opinion, I like manual piping because I don't believe it is as tedious as you describe, but it does give you an explicit picture of what happens, without magic underneath.

That said, there are tradeoffs. Neither top-down nor bottom-up is a clear winner and it's not fair to try naming a winner.

Top-down Bottom-up
Single source of truth Yes No
Explicit piping No Yes
Easy reusability of components No Yes

So, yes, with Nested Dataflow Components you need to manually pass sources and sinks from parent to child and vice-versa. But its a cost we pay in order to get better reusability and explicit contracts on the component boundaries.

If you really really want Redux or a single state atom approach, go ahead for it, but I don't think we can do a compromise between Nested Dataflow Components and single state atom because the two are opposed architectures.

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

@Cmdv Your data flow is going to be one massive mess though

I don’t agree.

I'm going to have to do a load more pipping make sure each child => parent relationship is correct.

Not really. It’s just passing properties.

@HighOnDrive
Copy link

"If you really really want Redux or a single state atom approach, go ahead for it, but I don't think we can do a compromise between Nested Dataflow Components and single state atom because the two are opposed architectures."

This needs to be explained a hell of a lot more. There was the Flux Challenge as well, what was learnt from that exercise? I agree that for complex apps a one atom state is useful. The whole problem here is that people are shooting off their ideals while only working on super small concept apps.

I propose that if your going to be so dogmatic at least provide a A vs. B app example, where one is done top-down and the other bottom-up. I'm sure @Cmdv and others can come up with the challenge, it would be interesting to then see the expert response from both sides of the fence.

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

@staltz I didn't realise what I'm trying to achieve is a top down approach.

Could this specific data handling be a function/driver? much like the LocalStorage driver but it would store state in an immutable object. Then this Object could be manipulated/listened to specified components that wish to listen/interact with it.

This would still result in piping but at least you wouldn't have to handle the parent => child part if you were a few levels deep.

The only reason I wanted to feed it to the Main and have that dispatch it through out the app was to simplify the complexity of pipping and a side effect was to automatically be able to have that data available at any level once fed in at the top.

But if I keep that functionality inside a function/driver. I think that I then wouldn't be moving away from the FP paradigm and it could just be seen as a glue function do you think this is a better approach?

@staltz
Copy link
Member

staltz commented Dec 6, 2015

because the two are opposed architectures."

This needs to be explained a hell of a lot more.

One is top-down, the other is bottom-up. What would be a compromise between those? Middle-center? That's why I don't think it's necessary to explain, it's pretty obvious top-down and bottom-up are opposites.

I propose that if your going to be so dogmatic at least provide a A vs. B app example, where one is done top-down and the other bottom-up.

I wrote a blog post about the ideas, and you can check Flux Challenge if you want.

Could this specific data handling be a function/driver?

You can put "stuff" in drivers, but that's not the point of drivers. As Nick Johnstone put it, "Drivers encapsulate imperative side effects". That's all there is to it. State is not an imperative side effect in my opinion.

Also, because the single state atom approach has state as the main piece of the application, you're not supposed to put main business logic in drivers. Drivers should be "plugins" to use imperative side effects, nothing else.

I know where you are trying to get, which is minimizing code for manual "piping". Drivers are not the way to achieve that, neither Nested components are. The way of achieving that is building an architecture that has only one MVI. It has the top-level M, the top-level V, and the top-level I. Then, "dumb components" are just simple pure functions from state to vtree. Model is the place where to keep state, even the global shared state in the case of the top-down approach.

If you find manual "piping" bad, please show us those parts of your code which you don't like, maybe we can find a way to refactor them. But I'm quite sure the explictness of the piping will remain because it's inherent to bottom-up approach. And there isn't a middle ground between bottom-up and top-down. Not as far as I know...

@HighOnDrive
Copy link

"I know where you are trying to get, which is minimizing code for manual "piping". Drivers are not the way to achieve that, neither Nested components are."

Hopefully there would be a better reason than that to require a one atom state! Architecture is also driven by plain old user/app functionality, the toolkits must bend to the rules of the user/app and so what the user wants in the end determines what the software is expected to do. If a given framework or API is to crippled to handle the demands then so be it, just never lose sight of what the user expects and needs because they are always right and the ones who will pay you for their good pleasure.

Is having a bottom-up preference not also at least a significant reason why there is a sudden interest in Most.js over RxJS? It seems to have been discovered/acknowledged that RxJS is to slow for all the piping required to enforce bottom-up dataflow ideals.

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

the toolkits must bend to the rules of the user/app and so what the user wants in the end determines what the software is expected to do.

A toolkit is composed of several tools, hopefully. The composition of the kit is entirely up the user. However, if the user wants a hammer to drill, the user should get a drill in the kit. Demanding that the hammer ought to do the job of a drill is just ridiculous.

It seems to have been discovered/acknowledged that RxJS is to slow for all the piping required to enforce bottom-up dataflow ideals.

Absolutely not. While Most.js is extremely fast, RxJS is by no means "too slow".

@HighOnDrive
Copy link

@Frikki I think you misunderstood my intention. I was just saying that frameworks and APIs are really only useful if they serve a purpose. In my creative world that purpose comes from imaginative and aspiring users. There is hence sometimes a gap between their vision and what present age software can actually do, any compromise would just not work or sell out the purpose.

Agreed, a craftsman needs more than one tool in their toolkit, also good engineering skills can't hurt in the tight spots.

In the end a muscle builder (framework) can take all the steroids (Most.js, etc) they like and look like the hulk. Yet, if they can not jump into action and save the lady or the day what is the point? There are apps to build and experiences to improve upon, hopefully we'll start to see heroic actions (apps) that were made with Cycle soon enough. That is if we can keep from tripping over our own top-down and bottom-up wiring.

@Frikki
Copy link
Member

Frikki commented Dec 6, 2015

@HighOnDrive We have already seen an app, power-ui, built with Cycle.js, and it uses the bottom-up approach. Whether it saved the lady of the day, we’ll have to ask @staltz and Futurice. Again, which approach is chosen, top-down or bottom-up, is up to the developers; both are entirely possible. Staltz has already outlined how to achieve top-down wiring above.

@HighOnDrive
Copy link

@Frikki Absolutely, we have a few scattered examples to dig through. When the new Cycle update happens what seems reasonable is to have some app templates produced. Just wireframe style templates that have no real contents but they wire up generic and chained drivers, routing and top-down or bottom-up scenarios. These do not have to be app starters, they would just clearly demonstrate the dataflow options.

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

@staltz Thanks for the in depth explanations. The only reason this came up is because I'm trying to imagine cycle replacing apps I work on a daily basis.

For example we have a Diagnostic flow with multiple routes through the application with multiple endings. (a total of about 30 pages) As you move through the app you pic up various pieces of information/data required.

  1. has visitor come via home page | if not redirect back to home
  2. is user logged in | determines options shown through out the flow
  3. did user select specific option | determines options shown through out the flow
  4. results of external api call | one of the steps checks your local weather and this information is later used
  5. information about which route they took | for analytical reasons
  6. Has the user already hit this end point

There are more but as a quick example, some pages need to use some data whilst others don't. All pages need to be aware of 1. You need to be able to keep the a list of the pages visited etc.... (sorry you get the point though)

So before this dataflow was a hard one to reason about when say a new implementation needed to be put in. As a new dev to the team it took me a while to start to understand how everything was talking to each other. (still don't fully understand).

Then we decided to use the idea of what Redux implements and having this global Object where we knew was the singular place to check for the current state of things and change those as and when need be. If you build a new page or implementation you know that what ever you are going to interact with is going to be orientated around this Global Object.

I know this is not what everyone would want from an app, but I hope it can it can be seen that dealing with data in this way still allows you start to build and expand as you wish.

But saying that I understand that this is seen as a top-down approach now and I'm going to go away and research some more experimenting because I don't think this is a straight forward black and white thing.

There are many languages that be seen as better than JS but the reason JS still does so well is because of the simplicity of using the language as a new comer. I know thats fully off topic but simplicity and easy to reason about are always good design choices I'd like to think :)

@Cmdv
Copy link
Contributor Author

Cmdv commented Dec 6, 2015

Ok I've been doing more digging and I can totally see what @staltz + @Frikki were trying to show me.

I came across this great article from Erik Meijer:

Imperative programs describe computations by repeatedly performing implicit effects on a shared global state. In a parallel/concurrent/distributed world, however, a single global state is an unacceptable bottleneck, so the foundational assumption of imperative programming that underpins most contemporary programming languages is starting to crumble. Contrary to popular belief, making state variables immutable comes nowhere close to eliminating unacceptable implicit imperative effects. Operations as ordinary as exceptions, threading, and I/O all cause as much hardship as simple mutable state.

I'll close the issue down now, thanks for the great insight 😺

@Cmdv Cmdv closed this as completed Dec 6, 2015
@staltz
Copy link
Member

staltz commented Dec 6, 2015

To be fair, if you have cases where many components need access to a common state, you can build those many as a single MVI, but still be able to use this MVI component in an even larger application. You know what I mean? No matter what internal structure your Cycle.js app uses, you can always embed it inside a larger Cycle.js app.

@HighOnDrive
Copy link

Thanks @Cmdv for that great link! As I'm learning to do more with monad pipelines the need for a central global data store is shrinking. I'm happy with a single top-down MVI DFC approach yet would like to decorate my app tree with many Christmas lights/widgets.

Seeing each observable like redux store with a reduce pipeline that can be as fancy as required, goes a long way. Being able to subscribe to any observable junction in a pipeline extends the options yet further.

I just think it's very important to produce real apps. The statement made in the comments of the linked article (quoted below) is a reminder that technical or academic excellence alone is not enough. If it was not for the real app I'm building I would surely lose my way, glad then that it serves as my north pole.

"Haskell was first made available in 1992, over 23 years ago, and according to Wikipeda, over 5,400 libraries and tools are available for Haskell programming. What language would be called a success that has more than 5,000 libraries of functions and not a single major program that was completely created in it? The only answer I know of is Haskell that was made and promoted by a group of publicly paid professors with next to no "real world experience" between them."

Can you tell that I am not one bit nostalgic or in need of baggage, my own app already tells me what is right and how I must proceed.

@tusharmath
Copy link

@staltz In the list of comparisons made between TopDown vs BottomUp, I think that one can have peer communication in a much easier way (or may be the only way) in TopDown approach.

For instance, if I have a component MAIN and it has 5 child components A, B, C, D where A,B,C,D need to communicate amongst each other. I have not found a clean approach that doesn't depend on intermediary Subject etc. Could you shed some light upon that?

@staltz
Copy link
Member

staltz commented May 6, 2016

I have not found a clean approach that doesn't depend on intermediary Subject etc.

@tusharmath You mean the proxy Subject technique?

@tusharmath
Copy link

@staltz Yes, referring to this discussion — #170
Using a proxy subject, seems like an implementation of a TOP => DOWN technique and looking at that discussion thread, it seems like its kind of impossible to do without it. This would mean that there are things that we can absolutely not do in a Bottom => UP approach, doesn't it?

@tusharmath
Copy link

@staltz We could infact have an immutable datastore driver, which would maintain all shared states at one place?

@staltz
Copy link
Member

staltz commented May 7, 2016

Bottom-up approach simply means you have built the child component first, and it manages its own state because you build it as if it would be the entire application. And only after that have you decided to compose it in the context of a parent. So Bottom-up doesn't mean that you can't compose children components. If the parent needs to maintain some common state for all its children, it's still a Bottom-up approach as long as you didn't start building the parent. If the children cannot live (be reused) in other applications, then it's not bottom-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants