Closed toranb closed 8 years ago
I haven't tried it yet but creating redux-unaware components should be possible with this solution. A redux-aware route or controller would pass state down along with ember closure actions to do the dispatching.
For the initial mapping of state to props - that's all manual for now and done at the route/controller level (though it be done in a component instead). I could see the benefit of some helpers like mapStateToProps
to simplify that.
An approach I'm experimenting with is not using closure actions at all - instead if I get a "connected" component w/ actions already mapped (dispatch) then I can hit the store directory (redux) and after the state is updated my "connected" component would just tell the ember component to "re-render"
Any reason I shouldn't take this approach?
<opinion>
I feel that sending actions up the component tree => then to the route (if needed) is painful and something I never felt doing React w/ Redux</opinion>
Am I alone here or do ember developers want to send actions up/ inject services /etc ?
Let me see if I understand you correctly. You'd essentially pass (or connect via some api) a dispatch function through to each component? Then just use that dispatch to affect the state? If that's the case, re-rendering would happen naturally if your component state is a computed property off of the redux state.
In that scenario, the components have to be dispatch aware, but wouldn't have to inject a service, right?
I'm with you on the action wiring opinion. On one hand, passing actions up creates a contract between the parent/child components. But the benefit of that contract seems a lot less when state is centralized. Personally, I don't see a ton of benefit in passing actions up vs simply dispatching them. More places for things to go wrong.
I couldn't have said it better myself - the single state tree is at the top of every component so why not dispatch a change to it directly. This also allows much easier shared state between components that are not necessarily "nested" but are on the page at the same time. In the past I've had to use a custom event bus (w/ help from Ember.Evented).
This approach not only eliminates the need for an event bus but also appears to be less error prone (as you have less moving parts / less configuration to wire around).
I'm playing around with an example app until I have all the parts needed to build something legit. Then I'll get more involved w/ this project to give back if it looks like I have something to contribute :)
https://github.com/toranb/ember-redux-example/commit/f3509b8da5ebd94caa6dd635d79a1dda90d19bd3
One correction - closure actions will still play a role (but redux does the heavy lifting).
One example of closure actions w/ redux and ember.
starting w/ a connected component called user-list (top of the route tree let's say)
This gets all the connected redux goods but this component doesn't render any of the actual markup itself (instead if pushes this into more presentation like components). Inside the user-list template you would pass down the actions you got from the connected func (after mapDispatchToProps runs).
I simulate this below by passing the grid component both the state (people) and the action to move someone down.
{{user-grid people=people down=(action "down")}}
Then inside the user-grid component you could use that w/out much effort (literally just wire it in the hbs)
{{#each people as |person|}}
{{/each}}
<button onclick={{down}}>down</button>
I took a first run at connect this morning and it's not terrible. This is obviously a first pass so no edge cases are yet covered but it should offer up the next step -why this is interesting to me. Now my components can be connected and redux unaware (honestly they are just pure functions now that render html -you can swap the data flow plumbing at another time w/out harming your components in any way).
Here is my new list component w/ a simple counter (of zero) and a button to let you increment that state
var stateToComputed = (state) => {
return {
low: state.low
}
};
var dispatchToActions = (dispatch) => {
return {
up: () => dispatch({type: 'UP'})
}
};
var UserListComponent = Ember.Component.extend();
export default connect(stateToComputed, dispatchToActions)(UserListComponent);
The full blown connect function is below if you want to give it a look. It's up and working (no tests yet so much todo's but it seems to match what I got from react-redux). I'll continue iterating the next 2 weeks until I get something I'm happy with / test-driven / etc
Just glad someone else in the ember community is interested in this project :)
https://github.com/toranb/ember-redux-example/commit/3de5bdc60d1bd8a9a8389d9881078e9b1d00fa1b
var connect = function(mapStateToComputed, mapDispatchToActions) {
return function wrapWithConnect(WrappedComponent) {
return Ember.Component.extend({
init() {
var model = this;
var store = this.store;
this._super(...arguments);
store.subscribe(() => {
Object.keys(mapStateToComputed(store)).forEach(function(key) {
model.notifyPropertyChange(key);
});
});
Object.keys(mapStateToComputed(store)).forEach(function(key) {
defineProperty(model, key, computed(function() {
return mapStateToComputed(store.getState())[key];
}).property());
});
Object.keys(mapDispatchToActions(store)).forEach(function(key) {
model['actions'] = {};
model['actions'][key] = mapDispatchToActions(store.dispatch)[key];
});
}
});
}
};
Oh! Very cool. I'm starting to see the advantages here. By exporting a connected component, you're providing the mapping between the what the component requires and what the redux store provides. The component is redux unaware as far as it's data and actions go. However it still requires knowledge of connect
.
In a project with a bunch of imported components, the developer would need to wrap those components, right?
// app/components/connected-modal
import someModal from 'some-modal'
import connect from 'connect'
const stateToComputed = (state) => {...};
const dispatchToActions = (dispatch) => {...};
export default connect(stateToComputed, dispatchToActions)(someFancyModal);
Seems reasonable. I'm looking forward to what you come up with. I'm interested in trying it out with my todomvc example. However with that app, I'm hoping to continue to rely on the emberData store for the models.
@matthewconstantine I've been cranking away the last day or so and finally have a full test suite for the connect function (using both integration and acceptance tests)! Lots of edge cases to work through yet I'm sure but now I have an easy way to throw real world scenarios at it (and tests to document what I find over time).
Next I took my first run at "async" with ember + redux ...
The only part I'm still not sure about (or happy with) is that my component is fetching the state when it's loaded (unlike ember that would traditionally load this in the model hook of the route). I still think the url will track state that can be passed into the component as a readonly prop (like user_id for example). Then this user_id can be used in the component to fetch the correct data async (if needed).
I'm trying to put into words why I don't like this (or why I think I don't like this)
1) I've been doing ember for 3 years and this is very different (read: unfamiliar) 2) The routes job has traditionally been to "fetch" state and pass it down ... so what's it's role now? 3) doing anything in the init method is out of the ordinary for me personally (maybe a lifecycle hook?) 4) using triggerAction feels / looks silly (I couldn't get sendAction to fire this)
Anyway -the async component I'm playing around with is below (simple: on load fetch some users)
var stateToComputed = (state) => {
return {
users: state.users.all
};
};
var dispatchToActions = (dispatch) => {
return {
fetch: () => ajax('/api/users', 'GET').then(response => dispatch({type: 'DESERIALIZE_USERS', response: response}))
};
};
var UserListComponent = Ember.Component.extend({
init() {
this._super(...arguments);
this.triggerAction({action: 'fetch', target: this});
}
});
export default connect(stateToComputed, dispatchToActions)(UserListComponent);
https://github.com/toranb/ember-redux-example/blob/master/app/components/user-list/component.js
I agree, doing fetches within a component strays pretty far from ember. One considerable downside to it is you lose the ability to pause a route transition. The only way I've seen to pause a transition is to return a promise from the model
method. Pausing is important for apps that rely on loading
routes. Sure, that can be mimicked manually with redux but I don't know what it gets us.
I think it makes a lot of sense for routes to be redux-aware then pass data down to components that may or may not be redux aware.
In my todomvc application route I store a promise in the state tree for the express purpose of pausing the transition.
I'm interested in finding some approaches to async that are less manual. redux-promise looks interesting.
@matthewconstantine I created a route function that acts much like the connect function but for Ember.Route instead of Ember.Component. With the implementation I have in place today it supports the pause and loading states for free (no need for another library to bridge the divide).
Below is what my ember route looks like for the users list endpoint
var model = (dispatch) => {
return ajax('/api/users', 'GET').then(response => dispatch({type: 'DESERIALIZE_USERS', response: response}));
};
var UsersRoute = Ember.Route.extend();
export default route(model)(UsersRoute);
Now my component doesn't fetch anything like it did yesterday (feeling much better about this)
var stateToComputed = (state) => {
return {
users: state.users.all
};
};
var UserListComponent = Ember.Component.extend();
export default connect(stateToComputed)(UserListComponent);
I implemented a complete master/detail with 2 routes, 2 components and 2 actions in the reducer (one to deserialize the LIST payload and another for the single user payload).
https://github.com/toranb/ember-redux-example/commit/b27d4483fb22131f6a2c1fd49696c39158ad462a
If you don't have time to look over that full blow commit, the route I extracted that solves this is nothing more than a way to call the usual model hook (ember) with dispatch (so redux can hit the reducer and return a new state for the subscribed component).
var route = function(model) {
var finalModel = model || function() {return {};};
return function wrapWithRoute(WrappedRoute) {
return WrappedRoute.extend({
store: Ember.inject.service('redux'),
model(...args) {
var store = this.get('store');
return finalModel.apply(this, [store.dispatch].concat(args));
}
});
};
};
export default route;
So good news but I now have 2 more questions ...
1) most master/detail examples in react + redux pass the JS model "clicked" directly (and the detail/or child component just takes that as a prop). In my example I had let the route do the ajax work and when it came time to "subscribe" for state I had to pull in ALL (the entire graph of users) and filter until I found the "selected" user
I didn't want to store another "single" object in the store so I just set "selected" when the link-to is clicked (inside the reducer action that is fired in the route to fetch that individual user model). The question is - I don't like subscribing to ALL and then filtering it down when in reality I only needed the one user and it would be nice if it was just "mapped" to computed (w/out additional work on my end).
How should this work from your view point?
var stateToComputed = (state) => {
return {
users: state.users.all,
selected: state.users.selected
};
};
var UserDetailComponent = Ember.Component.extend({
user: Ember.computed('users.[]', 'selected', function() {
const selected = this.get('selected');
return this.get('users').filter(function(u) {
return u.id === selected;
}).objectAt(0);
})
});
export default connect(stateToComputed)(UserDetailComponent);
2) Because the reducer is where I store state about the "selected" user I'm now technically duplicating this because the route/url params already has this data. When I looked at React Router + Redux I noticed some pain trying to keep the routers "data" in sync w/ redux and I'm curious what we can do in ember to make this less painful.
Options...
a) don't set "selected" in the reducer at all and instead push it down to the template (and then component). The component would in effect have that data as a prop but it wouldn't be stored in duplicate (reducer/store + route/url).
The downside here is that if I want my subscribed components state to be visible in one place using that single function it's suddenly split between that func and the template that passed in the route information. (harder for people to follow?)
b) write something to better keep the routes params + reducer/store in sync. (not my first choice)
https://github.com/rackt/redux-simple-router
The project above is the primary spot that shows the workflow
redux (store.routing) ↔ ??redux-ember-router?? ↔ ember-router
c) not worry to much about it
We could be facing bigger issues and maybe this just isn't priority one right now. Any time state is floating around in duplicate you almost always increase the likelihood that you will introduce a bug when they fall out of sync :(
If I understand it right, the question is whether it's the parent's responsibility to send a) just the selectedModel down to the component or b) send the collection + the selectedId to the child and let it figure it out.
That's a general architectural decision that applies to just about any component framework. I've dealt with that in ember apps as well as react apps. In this case the parent is a reducer (the route is just a passthrough) but the same problem applies if the parent is a route.
Option A (pass the selectedModel) might win if you're sending the selected model to multiple components. You save yourself form doing a filter within each component. More performant that way. And you're not sending more data than the component actually needs.
I'm generally not too worried about duplication when it's happening at the reducer level in a read-only data store.
Option B (pass the collection+selectedId) wins if your components actually need the collection. Each component has to do the filter but there are ways of improving that. The filter could be a computed macro. And there's nothing preventing a component from passing the selectedModel to sub components.
If the repeated filter is problematic, you could even go with Option C, collection+selectedIndex. It means that components don't need a filter function at all. And since this all backed by a read-only state structure, there's little risk here. Falcor does that kind of thing a lot. Pretty common in React as well.
Your route
example looks really interesting. What advantage do you see in wrapping a route vs. just defining it with knowledge of redux?
Oh and to answer the question about url params. I've been seeing the data flow as this: urlParams -> router -> route -> reduxStore. Then reduxStore -> everything else.
A well made app would ensure:
I think this is the sanest approach in most cases. One scenario it doesn't cover is what happens when the redux state is replaced by an external source. The state of the UI would reflect the new state but the url and urlParams would be left out. However, if the app follows the above rules, it might not matter what the URL is. Any further navigation would properly reset the url and params.
On the other hand, sharing the URL after a full replaceState would be problematic. So maybe we will need a bi-directional solution for updating the URL based on the redux state. I'm open to ideas.
If I understand it right, the question is whether it's the parent's responsibility to send a) just the selectedModel down to the component or b) send the collection + the selectedId to the child and let it figure it out.
After reading that reply I think I'm better able communicate my struggle with this example.
1) in pure components land (no route) you could send the element clicked down to a child /presentation component to have the detail html rendered. Then you get all the benefits of DDAU again -the child component would dispatch events to the parent to change any state and because the parent is a "connected" (smart component) it will get the update and push down the new "state" to the child (like you see in pure redux today).
2) with a route in the mix you instead get an outlet that is "filled in" after the route transition is complete. The web component injected is now a "top level" / "connected" (smart component) that would then itself have children (presentation) components. I can see some benefits of this approach but still can't help but feel like I've got it all wrong.
When I described the 2nd example above the "solution" I think of off the top is that the route itself should play the role of "connected" (smart component) and pass down props/actions to the web component. The problem in ember is that we usually don't hold on to state in the route like this and my first spike at it today resulted in a funky "half route/half component" (monster object) that I didn't want to show the world :(
I then decided to look at the problem again from a completely different view point. Using the route only to "dispatch" a state change to the reducer (as you've suggested above). This allows the route to be a simple state manager that keeps the url in sync and provides true back button support like we've come to expect from well constructed JS apps.
Next I removed the outlet from my user-list component and instead let that "connected" (smart component) pass down a user to the user-detail component when selected is set (by the ember route).
Now the detail web component is DDAU friendly. It only renders html and has all the state (props) passed in as well as the actions it can invoke (dispatch). The user-list component now contains the user computed (single place to compute this now) and passes it down.
I've only found 1 thing I dislike about this approach (so far).
I'm not using outlet and instead a conditional in user-list to determine if I should show the detail template. I hate this because the ember route (classic ember) wouldn't require a conditional at all. Maybe it's possible to do something else here that would let me keep the outlet but also supply "user" when selected is set on the user-list component?
And because of the conditional now I need to "reset" selected to null when I transition away from the detail route because the parent model function isn't called when you just navigate up (from detail to the list route). This won't be an issue when "routable components" land because the new "attributes" hook will be called each time you enter a route (so this isn't a huge priority at the moment).
Unlike today's model hook, attributes gets called every time your route is entered, whether or not transitionTo was passed a preexisting model. This eliminates the need for beforeModel and afterModel, because you can implement either behavior from within your own attributes or model functions. Therefore, beforeModel and afterModel are deprecated.
The biggest pain point is still having to use that conditional in the hbs. Any ideas around this or how to use outlet to achieve something like this instead? (keeping the same basic architecture in place of master/detail where the detail is a "presentation" component only rendering html with the data it's given)
{{#each users as |user|}}
<div class="user-name">{{user.name}}</div>
{{#link-to 'users.detail' user.id class='user-detail-link'}}details{{/link-to}}
{{/each}}
{{#if selected}}
{{user-detail model=user save=(action "save")}}
{{/if}}
Here is the full commit to see both the master/detail components in action but I'm sure you get the gist.
https://github.com/toranb/ember-redux-example/commit/dca3e53dbb7c446a1fac8977f0df0d5d1c4ca87a
Why one over approach over the other
You also asked about my crazy wrapped connect/route functions and why you might choose one over the other. I think this is part preference but I also have some rationale for it based on my experience working with younger engineers.
1) it constrains what the developer thinks is possible
Not always good as I'm sure we can both agree but one think I like about this abstraction is that I don't even know a service is injected (unless I look at the source). If I was new on some team using this approach (and we assume it's proven to be a good/ accepted way to build ember apps vNext) I would see a much smaller surface area /range of possible places that involve redux (or whatever is plumbing code is in charge of state changes).
When I feel this restriction I'm much less likely to "pull in the store and start dispatching things" in other places like init/setupController/etc. Over time if you begin to push past the limits (and my experience you can easily grab the store (as it's an injected service) and use it).
2) functional code is nice to compose apps with
Over the years I've personally come to love a function that takes data in and returns something consistently. Most of my ember apps already benefit from this in mixins/helpers/etc but having the core functions that tell me what state the app is in (or how to change it) just makes it that much easier to change over time.
Another win comes from the testing side of course. When I'm breaking a problem apart it's really handy to just call these functions (like model) and assert against the result because it's truly just a function.
At the end of the day it's still preference and I don't expect your addon/or the community to take what I enjoy as the silver bullet :)
I may even release my own addon that is the "functional" look at redux + ember and I'm hoping you don't take offense to the idea. I'm mostly in the learning phase right now and you've been a great person to share ideas with (even when I'm way off /completely wrong). If I haven't said "thank you" yet please know that our discussion has been the most rewarding of it's kind (in my short 10 years as a software professional).
Thanks again for taking time out of your weekend to read this crazy long rant (funny -this was the 3rd time I rewrote it today as I keep getting it half wrong).
@matthewconstantine after that long response I realized that in practice I really didn't like the "no outlets" idea I talked about above. Instead I reverted that commit and now think that the missing primitive I was looking for should live in the reducer (or another function that plays nice along side the reducer).
I started asking myself "what didn't you like about the outlet approach w/ redux" (from a few days ago) and truthfully it's that I filtered the users array (all users in the store) to find the one I needed. Was this all that terrible and how does something like ember-data do this today?
Instead what if I just had a function in the reducer that "found" the user selected and that was all I passed down to the detail component? This would eliminate one of the pain points we talked about - a single place to get the current user (a function in the reducer). No longer would each component need to filter down and find the selected user.
The trick w/ this master detail that got me all confused from the very beginning...
"yes- it's the detail from the parent component" but we also route to it and fetch the latest details about that user async (to ensure we get the most up-to-date information about that object). Because we fetch some state, this isn't the traditional "parent/child" web component context I've described over and over in this thread ... yet I'm acting like it "should be".
I'm starting to think that when you "route" to a component (another view in the app) it's almost always the case that the top of that new web component tree will be the "connected" component. And w/ ember I think this makes a ton of sense actually.
1) enter a route, fetch data for that view (sync or async) 2) when that model hook is returned we spin up a web component 3) the above web component has chosen to "subscribe" to redux changes 4) if any state changes across the app the component will "re-render" if needed
I really feel this has been something missing (for me personally) since the early days of ember. The role/responsibility of redux feels like a natural fit now (all in my head again but you get it).
1) ember route will manage how you transition in/out of web components 2) redux store is the single place to hold all the state for the app 3) redux reducers manage the changes to that state over time 4) connected web components subscribe to changes in the store and re-render when necessary 5) non connected web components simply render html
I think it's starting to click for me now :) feeling really good about the next steps in this adventure!
You've got it. I think the reducer is a natural place to keep selectedItem. And it's safe too, assuming the urlParams are upstream of the reducer. From what I've seen so far, it seems the reducer can replace computed properties in a lot of cases. I find it interesting that in the React world we see very little call for computed properties the way they are in Ember. I think solutions like Redux reveal why.
The flow you outlined makes a lot of sense. I've been envisioning a similar flow and have been delighted by how well Redux works with Ember. It feels like something I didn't know I was missing. And from what I've seen it plays quite well with ember-data. My current solution using ember-data has some side effects, but I imagine there are probably ways to eliminate them.
I'm interested in your FP take on redux+ember. I think the two styles are complementary. I'd love to try out your connect
and route
functions. Would you be up for making a lib with them? There's one difference I'd need to resolve if I did. With ember-cli-redux I'm trying to preserve the ability to use ember-data. So if redux is injected on object.store, we'd lose that ability.
Keeping ember-data in the loop allows developers to ease an application into using redux. I've found there's a really nice result of this is: by moving all of the ember-data operations into the reducers, ember-data can be entirely abstracted from the view layer. We get all the querying and relational benefits of it but could easily replace it in parts of the app that don't benefit from it.
Thanks for your thoughtful answer about the FP approach. Those benefits make a lot of sense. I partly asked the question because connect
does introduce another point of failure. A developer has to be aware of and properly maintain the dispatchToActions
and stateToComputed
mappings. But on the other hand, it eliminates more places for things to go wrong (like calling dispatch from an unstable place). And the testing story is certainly a bonus. It'd be nice to not have to mock a redux-store when testing a component.
And thanks for the kind words, a big goal in me open-sourcing this was to get some conversations started with like-minded engineers. I strongly believe that Ember needs a good state management story, whether it's this or not. There's just too much useful tech that relies on it. I'd love to see hot module replacement, time traveling debugging and state mirroring make their way to Ember. In the meantime, I'm happy if we can prototype apps faster, put routes on a diet and push async to the edges :).
About the ember-data plug n' play with redux
I'm not sure this is my first go-to
but I'm willing to hear how they work together. So far the dispatch flow for async data seems to replace some of what I would accomplish w/ ember-data. In addition, I'm planning to understand how rollback/dirtyTracking works when redux is in the mix. I like the idea that relationships are "free" (ish) but I'm curious what pros/cons you find using this approach (redux) with the core of ember-data (the model anyway).
When you ask "would you be up for making a lib with them" -I've got a fairly basic suite to capture the requirement but as you know it's early days (lots of learning seems to happen with each day). If you want I'm truly open to have you take what I've shown so far and iterate on it (add it here if you see fit/ or if you don't like the current form you should/can evolve it).
I'd love to see what you find works (and what works w/ some existing addon code like ember-data).
This week I've got plans to start looking at the async + relationship story (absent of ember-data) just to get an idea of what I'm up against. I've done book keeping work w/out ember-data in ember-cli-simple-store but I'm honestly curious how redux changes my view on "dirtyTracking" and rollback behavior (since we have immutability in the mix).
I'll keep you informed as usual :) forever learning and improving the data flow that we can achieve w/ ember!
I'm curious what pros/cons you find using this approach (redux) with the core of ember-data (the model anyway).
It's mostly a pragmatic reason. At AltSchool, we have tons of models with well defined relationships. Those models are shared across a number of ember apps ranging from tiny to enormous. Ember Data along with our own query language gives us a lot of flexibility in our apps. For a new project with new models, however, there might not be any reason at all to use redux+ember-data.
With redux+ember-data we lose some of the benefits of immutable state and pure reducers. I hope we'll find ways to increase purity that as this matures. But even at this stage, the benefits seem to outweigh the risks.
Regarding connect
and route
, I'll iterate on that and see what I can do. The more I think about it the more those make sense to support here. The benefits for testing, wrapping imported components, etc. are definitely interesting!
@matthewconstantine awesome! from the sound of it we can close out this issue finally :)
The react-redux bindings offer a way to create "connected" or "redux aware" components so you can map state and dispatch directly.
From the looks of this and other ember redux attempts it seems the preferred approach is injecting the full on store (to getState/ dispatch/ etc).
Any plans to look at the big wins you get using components that are unaware of redux altogether?