-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Feeback wanted: Rewrite with ideas from @acdlite #46
Conversation
import TodoApp from './TodoApp'; | ||
import * as stores from '../stores/index'; | ||
|
||
@dispatch(composeStores(stores)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
composeStores
will turn a stores
exported object into a normal Store with state, action => state
signature that would combine them. It's a good shortcut, and you may replace it with a custom mechanism like a single Immutable Map if you want to. cc @fisherwebdev
Happy Birthday! |
const { children, actions: _actions } = this.props; | ||
const { atom } = this.state; | ||
|
||
const actions = Object.keys(_actions).reduce((result, key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lodash's mapValue
would be more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, might use that. I kinda like the rawness though (I didn't realize I could use reduce
like this).
I don't have strong opinion here. (Note it's also used in combineStores
)
@ooflorent I addressed both your comments |
So, the things still bugging me:
|
I pushed some perf optimizations: b70739a. // Before
export default class CounterApp {
render() {
return (
<Injector actions={CounterActions}>
{({ state: { counter }, actions }) =>
<Counter counter={counter} {...actions} />
}
</Injector>
);
}
}
// After
function select(state) {
return state.counter;
}
export default class CounterApp {
render() {
return (
<Injector actions={CounterActions}
select={select}>
{({ state, actions }) =>
<Counter counter={state} {...actions} />
}
</Injector>
);
}
} Note that this also frees you from destructuring inside The What do you say? If you're fine, I'd be happy to merge a PR in this branch implementing |
Note: I renamed |
Happy birthday! You must be truly coming from future or different universe... |
I added a This lets you reach into the global state as deep as you want and return an object that will be shallowly compared on each change. If the object is shallowly equal, By default, |
static contextTypes = { | ||
redux: PropTypes.object.isRequired | ||
}; | ||
|
||
static propTypes = { | ||
children: PropTypes.func.isRequired, | ||
select: PropTypes.func.isRequired, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about:
select: oneOfType([ string, func ]).isRequired
And then:
import get from 'lodash/object/get'
import isFunction from 'lodash/lang/isFunction'
const slice = isFunction(this.props.select)
? this.props.select(atom)
: get(atom, this.props.select)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that.. But then, people are going to want arrays of string keys for keyPaths and we're back to cursors :-). I think functions (or lambdas) work great here.
So here's what I got API-wise: Decorator versionimport React from 'react';
import { inject } from 'redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';
@inject({
actions: CounterActions,
select: state => ({
counter: state.counter
})
})
export default class CounterApp {
render() {
return (
<Counter {...this.props} />
);
}
} Component versionimport React from 'react';
import { Injector } from 'redux';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import * as TodoActions from '../actions/TodoActions';
export default class TodoApp {
render() {
return (
<Injector actions={TodoActions}
select={state => state.todos}>
{this.renderChild}
</Injector>
);
}
renderChild({ state, actions }) {
return (
<div>
<AddTodo {...actions} />
<TodoList todos={state} {...actions} />
</div>
);
}
} Is the state/action merging too implicit? Can we make it better now that we have |
Since the import React from 'react';
import { inject } from 'redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';
@inject((state, wrap) => ({
counter: state.counter,
...wrap(CounterActions)
})) |
👍 for select |
I don't quite like this Maybe do it like this? import React from 'react';
import { inject, perform } from 'redux';
import Counter from '../components/Counter';
import * as CounterActions from '../actions/CounterActions';
@inject(state => ({
counter: state.counter
}))
export default class CounterApp {
const { dispatch, counter } = this.props;
render() {
return (
<Counter counter={this.props.counter}
{...perform(dispatch, CounterActions)} />
);
}
} import React from 'react';
import { Injector, perform } from 'redux';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import * as TodoActions from '../actions/TodoActions';
export default class TodoApp {
render() {
return (
<Injector select={state => state.todos}>
{this.renderChild}
</Injector>
);
}
renderChild({ state, dispatch }) {
const actions = perform(dispatch, TodoActions);
return (
<div>
<AddTodo {...actions} />
<TodoList todos={state} {...actions} />
</div>
);
}
} This would also fix #45. The difference with the previous suggestion is that, because
|
Since you are extracted |
Yes, thank you. |
This looks really great. I dig the The only thing that's stopping this from being 100% isomorphic is when you do |
perform(actionCreator, ...args) {
const action = actionCreator(...args);
return typeof action === 'function'
? action(this.perform, this.atom)
: this.dispatch(action);
} Usage example: function syncAction() {
return {
type: 'A'
}
}
function asyncAction() {
return perform => {
perform(syncAction())
}
}
function anotherAsyncAction() {
return perform => {
perform(asyncAction())
}
}
dispatcher.perform(anotherAsyncAction()) |
Missed that perform shouldn't call actionCreator itself, it takes an action: perform(action, ...args) {
return typeof action === 'function'
? action(this.perform, this.atom)
: this.dispatch(action);
} => export default function bindActions(actionCreators, dispatcher) {
return mapValues(actionCreators, actionCreator =>
(...args) => dispatcher.perform(actionCreator(...args))
);
} |
@vslinko I need to go now, can you PR your suggested changes to action creator calls relative to this branch? |
@gaearon I'll try |
Can you elaborate on this? What do I need to change? |
Simplify perform API
No, It's ready. |
createDispatcher should return binded functions
As is, the isomorphism story is already way better than any other Flux library, but if you want out-of-the-box isomorphism with no extra set-up — the same way hot reloading "just works" — then you'd have to move |
Unfortunately I don't have expertise here so can't comment. Can you give me an idea of how that would be used on the server side? I know how (de)hydrate API can be used, but I'm not sure what you're suggesting. (on server, there is no mounting so no prop lifecycle, is there?) |
It's not a huge deal, but currently on the server the user would need to make certain they do app.get("/", function(req, res) {
const dispatcher = createDispatcher(store);
res.send(
React.renderToString(<Provider dispatcher={dispatcher} />)
);
}); Not bad at all, but someone could accidentally making the mistake of calling I think it would be nice to have this option app.get("/", function(req, res) {
res.send(
React.renderToString(<Provider store={store} />)
);
}); and call |
How would you solve this API-wise while still providing the (de)hydrate API? |
👎 for |
For extra APIs like dehydrate, then yes, passing the dispatcher is the way to go. I personally don't think supporting both APIs is that bad (it's only two props total!), and I like the simplicity of just passing a store, but it's not a huge deal to me. |
Also, my perspective is admittedly a bit skewed because in my gist, literally the only stateful part is the atom, whereas Redux also keeps track of subscriptions (for good reasons) and needs to provide a direct interface for dehydration, etc. |
This is out as 0.8.0. |
So @acdlite got me a birthday present (yay it's my birthday today :-): this gist.
It shows how much easier Redux can be implemented if we embrace single-atomness and instead of hiding it, make it explicit.
I changed a few things from his implementation:
atom
down thecontext
, I pushsubscribe()
to avoid shouldComponentUpdate problems;setState
, maintainthis.atom
in Dispatcher sosetState
is called both on the root and the children at the same time and the atom is never stale in the action creators;combineStores
higher-order store (lol) that converts an object map of Stores into a Store function that combines them.I combined our two examples to better show how this works together.
I like it.
Would love to hear your thoughts.
@acdlite @ooflorent @threepointone @vslinko
TODO