Redux's relation to cursors #155

Closed
vjpr opened this Issue Jun 21, 2015 · 13 comments

Comments

7 participants
@vjpr

vjpr commented Jun 21, 2015

I have been looking at some interesting Om-inspired cursor-based approaches to storing state such as Morearty and Baobab.

Redux seems to be similar in that everything is stored in one tree, but does not have the ability to listen to events on sub-trees.

I was wondering if you could provide some comment on Redux's approach, and the similarities and differences to these specific cursor-based apporaches. Is there anything possible with cursor-based approaches that's not possible with Redux?

@ntkoso

This comment has been minimized.

Show comment
Hide comment
@ntkoso

ntkoso Jun 21, 2015

I've used Morearty. My main pain point was that cursors are too low level.
To change state you need to mutate cursor. To change other part of the tree you need to listen for sub-tree changes or manually mutate additional cursor in the same place.
In order to separate components and cursor mutation i've used Flux.
Stores were given access to Morearty's state.
Components were firing actions instead of mutating cursors.
'mutate another part of the tree' problem was removed by Flux. When action was fired, multiple stores were getting it and each store was mutating cursor to different parts of the tree.
As a result my components were using cursors only to get data and all mutations were easily traceable.

Redux has the same idea.

WHAT to change:
cursors: cursor = state.sub(pathOnTheTree)
redux: reducer(state, {type, ...payload})

HOW to change:
cursors: cursor.set(payload)/update(payload)/etc...
redux: reducer(state, {type, ...payload})

ntkoso commented Jun 21, 2015

I've used Morearty. My main pain point was that cursors are too low level.
To change state you need to mutate cursor. To change other part of the tree you need to listen for sub-tree changes or manually mutate additional cursor in the same place.
In order to separate components and cursor mutation i've used Flux.
Stores were given access to Morearty's state.
Components were firing actions instead of mutating cursors.
'mutate another part of the tree' problem was removed by Flux. When action was fired, multiple stores were getting it and each store was mutating cursor to different parts of the tree.
As a result my components were using cursors only to get data and all mutations were easily traceable.

Redux has the same idea.

WHAT to change:
cursors: cursor = state.sub(pathOnTheTree)
redux: reducer(state, {type, ...payload})

HOW to change:
cursors: cursor.set(payload)/update(payload)/etc...
redux: reducer(state, {type, ...payload})

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jun 21, 2015

Collaborator

You can implement read-only cursors on top of Redux very easily. Just listen to the root changes, select a specific path and compare if the reference has changed since the last time.

In fact that's why Connector accepts a "select" prop. That's a function that lets you query a slice of the global state. Because it's a function, it is composable: you can make your own helper that lets you make nested "select"s just like you can nest read cursors. See NuclearJS's "getters" for an example of this approach.

What Redux does not give you is write cursors. This is a core design decision made for a reason.

Redux lets you manage your state using composition. Data never lives without a reducer ("store" in current docs) that manages that data. This way, if the data is wrong, it is always traceable who changed it. It is also always possible to trace which action changed the data.

With write cursors, you have no such guarantees. Many parts of code may reference the same path via cursor and update it if they want to.

This is similar to how React works. If you see a DOM node, you can trace which component owns it. If you remove the component, there will be no DOM node.

Redux provides similar guarantees for reducers and data. So indeed, it is "less powerful" than cursors in the same sense React's model is "less powerful" than jQuery DOM manipulation. But sometimes less power is actually a good thing.

Collaborator

gaearon commented Jun 21, 2015

You can implement read-only cursors on top of Redux very easily. Just listen to the root changes, select a specific path and compare if the reference has changed since the last time.

In fact that's why Connector accepts a "select" prop. That's a function that lets you query a slice of the global state. Because it's a function, it is composable: you can make your own helper that lets you make nested "select"s just like you can nest read cursors. See NuclearJS's "getters" for an example of this approach.

What Redux does not give you is write cursors. This is a core design decision made for a reason.

Redux lets you manage your state using composition. Data never lives without a reducer ("store" in current docs) that manages that data. This way, if the data is wrong, it is always traceable who changed it. It is also always possible to trace which action changed the data.

With write cursors, you have no such guarantees. Many parts of code may reference the same path via cursor and update it if they want to.

This is similar to how React works. If you see a DOM node, you can trace which component owns it. If you remove the component, there will be no DOM node.

Redux provides similar guarantees for reducers and data. So indeed, it is "less powerful" than cursors in the same sense React's model is "less powerful" than jQuery DOM manipulation. But sometimes less power is actually a good thing.

@jdeal

This comment has been minimized.

Show comment
Hide comment
@jdeal

jdeal Jun 21, 2015

What Redux does not give you is write cursors.

@gaearon Do you see that as a philosophical/pattern stance or as a technical one? In other words, is Redux somehow preventing write cursors or otherwise causing the developer to fall into the pit of success?

For example, if I created a single action set that took a path as a parameter and passed that around to my components, would I have implemented the same thing as a write cursor? Or would that somehow still be fundamentally different than a cursor?

jdeal commented Jun 21, 2015

What Redux does not give you is write cursors.

@gaearon Do you see that as a philosophical/pattern stance or as a technical one? In other words, is Redux somehow preventing write cursors or otherwise causing the developer to fall into the pit of success?

For example, if I created a single action set that took a path as a parameter and passed that around to my components, would I have implemented the same thing as a write cursor? Or would that somehow still be fundamentally different than a cursor?

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Jun 21, 2015

Contributor

@gaearon without cursors how do you solve data binding on text inputs?

In Atom-React I permit to use valueLink on cursors:
https://github.com/stample/atom-react/blob/master/examples/todomvc/js/components/TodoTextInput.react.js

valueLink={this.linkCursor(this.props.textInputCursor)}

I find this handy, and it is actually my only real need for cursors, as in this kind of case they are much more convenient than firing actions/events for input keystrokes.

One could argue that I can use local component state for text inputs. That's true and I may do this in the future as my text inputs are becoming less responsive over time, but I think keeping everything in an immutable state is stilll a simpler model and try to keep it this way until the limits are reached.

Also I don't really understand the absolut need for listeners on cursors. If cursors are simply a path and a ref (like a lens with a ref), to the data structure, the only needed listener is the one on the root of the data structure, listening for swaps. Using listeners on cursor does not seem to be a requirement from me, at least if you always re-render from the very top (which may be less performant).

I can't really understand the need for read-only cursors. Why not directly pass the data if you don't need to write?

Contributor

slorber commented Jun 21, 2015

@gaearon without cursors how do you solve data binding on text inputs?

In Atom-React I permit to use valueLink on cursors:
https://github.com/stample/atom-react/blob/master/examples/todomvc/js/components/TodoTextInput.react.js

valueLink={this.linkCursor(this.props.textInputCursor)}

I find this handy, and it is actually my only real need for cursors, as in this kind of case they are much more convenient than firing actions/events for input keystrokes.

One could argue that I can use local component state for text inputs. That's true and I may do this in the future as my text inputs are becoming less responsive over time, but I think keeping everything in an immutable state is stilll a simpler model and try to keep it this way until the limits are reached.

Also I don't really understand the absolut need for listeners on cursors. If cursors are simply a path and a ref (like a lens with a ref), to the data structure, the only needed listener is the one on the root of the data structure, listening for swaps. Using listeners on cursor does not seem to be a requirement from me, at least if you always re-render from the very top (which may be less performant).

I can't really understand the need for read-only cursors. Why not directly pass the data if you don't need to write?

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jun 21, 2015

Collaborator

@jdeal

Do you see that as a philosophical/pattern stance or as a technical one? In other words, is Redux somehow preventing write cursors or otherwise causing the developer to fall into the pit of success?

For example, if I created a single action set that took a path as a parameter and passed that around to my components, would I have implemented the same thing as a write cursor? Or would that somehow still be fundamentally different than a cursor?

That's a great question! You can totally do that.

What I'm saying is that cursors are very low-level API. Just like you can have a single React component for your whole application, you can have a single SET action and a single reducer that behaves akin to a cursor. In my experience it's not very practical but you can definitely do this.

One important difference is that your SET action will still flow through Redux's dispatcher which potentially allows us to implement things like time travel (#113) and logging/other middleware (#63) that stands between your action and the actual data. With a vanilla cursor approach, a framework just doesn't have the power to do something like that.

Collaborator

gaearon commented Jun 21, 2015

@jdeal

Do you see that as a philosophical/pattern stance or as a technical one? In other words, is Redux somehow preventing write cursors or otherwise causing the developer to fall into the pit of success?

For example, if I created a single action set that took a path as a parameter and passed that around to my components, would I have implemented the same thing as a write cursor? Or would that somehow still be fundamentally different than a cursor?

That's a great question! You can totally do that.

What I'm saying is that cursors are very low-level API. Just like you can have a single React component for your whole application, you can have a single SET action and a single reducer that behaves akin to a cursor. In my experience it's not very practical but you can definitely do this.

One important difference is that your SET action will still flow through Redux's dispatcher which potentially allows us to implement things like time travel (#113) and logging/other middleware (#63) that stands between your action and the actual data. With a vanilla cursor approach, a framework just doesn't have the power to do something like that.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jun 21, 2015

Collaborator

@slorber

without cursors how do you solve data binding on text inputs?

Just like in normal Flux. Subscribe to a state slice, fire actions on change. It's not really that much different from cursors. The difference is instead of directly causing change, you need to express it as an Action, so that it's possible to do cool things like go back in time, or undo actions from advanced devtools. But the result is the same: the state is changed.

One could argue that I can use local component state for text inputs. That's true and I may do this in the future as my text inputs are becoming less responsive over time, but I think keeping everything in an immutable state is stilll a simpler model and try to keep it this way until the limits are reached.

I'd probably use local state but you're right it's worth to try pushing its boundaries. I know @chenglou has been experimenting with different ideas for React state. I'm potentially interested in an idea of “local” stores defined by components and attached/detached from the root Redux instances while the app is running, while still living in the single state tree and thus having the benefits of Redux: action replay, logging, etc. So I'm with you here and it's something I'd like to explore later on.

Using listeners on cursor does not seem to be a requirement from me, at least if you always re-render from the very top (which may be less performant).

We don't always re-render from the top for performance reasons. Also re-rendering from the top only works if you always pass the props down explicitly, which we don't do in Redux. We're using context, but its changes don't propagate reliably down the tree (at least in React 0.13), so that's why we need sideways subscriptions. It's not perfect, but it's a pragmatic decision.

I can't really understand the need for read-only cursors. Why not directly pass the data if you don't need to write?

Yeah exactly. That's why I'm saying you don't really need cursors with Redux. The only use case for “read-only” cursors is nesting (component A receives some path and gives component B some subpath without thinking about the current path) but it's solved by composing select functions (as I said, equivalent to getters in NuclearJS).

Collaborator

gaearon commented Jun 21, 2015

@slorber

without cursors how do you solve data binding on text inputs?

Just like in normal Flux. Subscribe to a state slice, fire actions on change. It's not really that much different from cursors. The difference is instead of directly causing change, you need to express it as an Action, so that it's possible to do cool things like go back in time, or undo actions from advanced devtools. But the result is the same: the state is changed.

One could argue that I can use local component state for text inputs. That's true and I may do this in the future as my text inputs are becoming less responsive over time, but I think keeping everything in an immutable state is stilll a simpler model and try to keep it this way until the limits are reached.

I'd probably use local state but you're right it's worth to try pushing its boundaries. I know @chenglou has been experimenting with different ideas for React state. I'm potentially interested in an idea of “local” stores defined by components and attached/detached from the root Redux instances while the app is running, while still living in the single state tree and thus having the benefits of Redux: action replay, logging, etc. So I'm with you here and it's something I'd like to explore later on.

Using listeners on cursor does not seem to be a requirement from me, at least if you always re-render from the very top (which may be less performant).

We don't always re-render from the top for performance reasons. Also re-rendering from the top only works if you always pass the props down explicitly, which we don't do in Redux. We're using context, but its changes don't propagate reliably down the tree (at least in React 0.13), so that's why we need sideways subscriptions. It's not perfect, but it's a pragmatic decision.

I can't really understand the need for read-only cursors. Why not directly pass the data if you don't need to write?

Yeah exactly. That's why I'm saying you don't really need cursors with Redux. The only use case for “read-only” cursors is nesting (component A receives some path and gives component B some subpath without thinking about the current path) but it's solved by composing select functions (as I said, equivalent to getters in NuclearJS).

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jun 21, 2015

Collaborator

I'm closing this, as I believe I clarified our position on cursors enough in the preceding messages.
We'll make sure to include this in the new docs (#140).

Collaborator

gaearon commented Jun 21, 2015

I'm closing this, as I believe I clarified our position on cursors enough in the preceding messages.
We'll make sure to include this in the new docs (#140).

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Jun 22, 2015

Contributor

Actually when calling set on a cursor you could make it fire a "cursor_set_action" or something like that, and register a reducer to update the data structure. This preserves the replayable action log feature.

This may seem a bit hackish as it is soomehow "internal framework actions", but what you want to do to mount locat state in the data structure seems to be almost the same kind of stuff.
@gaearon I think cursors are practical to bind to form inputs and things like that. We also used them to be used instead of "local component state" for which we don't necessarily want to publish in an action. (like hovered state)

Anyway I guess my usecase of form inputs will be solved with what you are working on, as I could then bind the input value to the local state instead of a cursor :)

But in my experience after working with them for a while, this created some confusion for our developers to not really understand when to use or not a cursor. So they use them a bit everywhere while simply passing the data would be largely enough. We end up with some very simple components that display a single line of text, taking a cursor as a prop :(

I agree with the performance need for listeners, as I start to see the limits of always re-rendering from the top: I need to be more and more careful about performance of some components, like the top-level layout components, that are rendered everytime.

Contributor

slorber commented Jun 22, 2015

Actually when calling set on a cursor you could make it fire a "cursor_set_action" or something like that, and register a reducer to update the data structure. This preserves the replayable action log feature.

This may seem a bit hackish as it is soomehow "internal framework actions", but what you want to do to mount locat state in the data structure seems to be almost the same kind of stuff.
@gaearon I think cursors are practical to bind to form inputs and things like that. We also used them to be used instead of "local component state" for which we don't necessarily want to publish in an action. (like hovered state)

Anyway I guess my usecase of form inputs will be solved with what you are working on, as I could then bind the input value to the local state instead of a cursor :)

But in my experience after working with them for a while, this created some confusion for our developers to not really understand when to use or not a cursor. So they use them a bit everywhere while simply passing the data would be largely enough. We end up with some very simple components that display a single line of text, taking a cursor as a prop :(

I agree with the performance need for listeners, as I start to see the limits of always re-rendering from the top: I need to be more and more careful about performance of some components, like the top-level layout components, that are rendered everytime.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jun 22, 2015

Collaborator

Actually when calling set on a cursor you could make it fire a "cursor_set_action" or something like that, and register a reducer to update the data structure. This preserves the replayable action log feature.

That's what @jdeal suggested above and I replied :-).

Collaborator

gaearon commented Jun 22, 2015

Actually when calling set on a cursor you could make it fire a "cursor_set_action" or something like that, and register a reducer to update the data structure. This preserves the replayable action log feature.

That's what @jdeal suggested above and I replied :-).

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Jun 22, 2015

Contributor

ah yes I missed that )

Contributor

slorber commented Jun 22, 2015

ah yes I missed that )

@idibidiart

This comment has been minimized.

Show comment
Hide comment
@idibidiart

idibidiart Sep 14, 2015

Take this in context of me looking for inaccuracies in my understanding of Redux and related concepts:

If I understand Redux correctly (just starting to look at it, so making lots of assumptions here) there seems to be a very subjective and almost artificial distinction between cursors and read-only subscriptions to subtrees combined with action dispatch that results in state change. Cursors can be functions or even methods on cursor classes that take the select argument in the constructor and return a cursor to the subtree with some custom behavior like what @slorber said (fire set actions etc) ... and they can have in and out transducers or whatever you want. The name cursor would normally mean ability to traverse a structure much like a cursor in say IndexedDB (moving up and down the rows) or even a keyboard cursor (moving up, down, left and right thru an ordered tree) but the way "cursors" show up in popular culture a la Om and other projects is akin to the concept of "lenses" ... or at least that is my own understanding.

The interesting thing is the behavior of the action dispatch, whether it is sync or async. In similar architectures to Redux, we've seen a problem with controlled components like text input when using async and typing fast and seemingly losing characters (due to delayed state change relative to actual input and the reaction to a delayed state transition) I assume the dispatch method in Redux is sync not async.

I would appreciate it if you could weed out all the fallacies in my take so far... or clarify things for me. Thank you. Redux is good work @gaearon

Marc

Take this in context of me looking for inaccuracies in my understanding of Redux and related concepts:

If I understand Redux correctly (just starting to look at it, so making lots of assumptions here) there seems to be a very subjective and almost artificial distinction between cursors and read-only subscriptions to subtrees combined with action dispatch that results in state change. Cursors can be functions or even methods on cursor classes that take the select argument in the constructor and return a cursor to the subtree with some custom behavior like what @slorber said (fire set actions etc) ... and they can have in and out transducers or whatever you want. The name cursor would normally mean ability to traverse a structure much like a cursor in say IndexedDB (moving up and down the rows) or even a keyboard cursor (moving up, down, left and right thru an ordered tree) but the way "cursors" show up in popular culture a la Om and other projects is akin to the concept of "lenses" ... or at least that is my own understanding.

The interesting thing is the behavior of the action dispatch, whether it is sync or async. In similar architectures to Redux, we've seen a problem with controlled components like text input when using async and typing fast and seemingly losing characters (due to delayed state change relative to actual input and the reaction to a delayed state transition) I assume the dispatch method in Redux is sync not async.

I would appreciate it if you could weed out all the fallacies in my take so far... or clarify things for me. Thank you. Redux is good work @gaearon

Marc

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 15, 2015

Collaborator

but the way "cursors" show up in popular culture a la Om and other projects is akin to the concept of "lenses" ... or at least that is my own understanding.

All I'm saying is that you wouldn't use such abstractions to write in Redux. You can use cursors or lenses or selectors or whatever to read fine, yep.

I assume the dispatch method in Redux is sync not async.

Yes, it's synchronous unless you're using a middleware to make it asynchronous on purpose. Please see the source.

Collaborator

gaearon commented Sep 15, 2015

but the way "cursors" show up in popular culture a la Om and other projects is akin to the concept of "lenses" ... or at least that is my own understanding.

All I'm saying is that you wouldn't use such abstractions to write in Redux. You can use cursors or lenses or selectors or whatever to read fine, yep.

I assume the dispatch method in Redux is sync not async.

Yes, it's synchronous unless you're using a middleware to make it asynchronous on purpose. Please see the source.

@denis-sokolov

This comment has been minimized.

Show comment
Hide comment
@denis-sokolov

denis-sokolov Mar 25, 2016

redux-cursor project might be of interest to readers in this issue. It’s an implementation of cursors avoiding the primary disadvantage gaearon notes.

redux-cursor project might be of interest to readers in this issue. It’s an implementation of cursors avoiding the primary disadvantage gaearon notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment