-
Notifications
You must be signed in to change notification settings - Fork 16
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
Should detect whole state spreading #5
Comments
warning in the console is really less than ideal. it basically is an issue similar to forgetting to do immutability, but maybe worse without the console warning. i hope #1 is stable and performant. |
or, error in console. In our test environment any error colours everything in red. |
PS: Please bump the library version, 1.2.0 could be much more performant than 1.1.6 due to new API from proxyequal and 1.2.1 works on IE11/Android. |
i put it to 1.2.1--is that ok? ps. i also decided to add a 2nd arg for https://codesandbox.io/s/64mzx7vjmz?module=%2Fsrc%2Fconnect%2Findex.js |
"performance" here is simple - you have to re-render component only if first part(mapStateToProps) got updated, rest - you dont care. |
Typically, a new const mapDispatch = (dispatch, props) => {
const doSomething = (arg) => dispatch({ type: 'FOO', payload: { arg, bar: props.bar } })
return { doSomething }
} ...unless they execute something like the following: props.doSomething = (...args) => mapDispatch(dispatch, this.props).doSomething(...args)
<button onClick={() => props.doSomething(123)}> Is that what you mean by "get the names"? It simply needs to know the names to make the above assignment to If this is true, then the performance is very good for |
Take a look here - https://github.com/reactjs/react-redux/blob/master/src/connect/selectorFactory.js#L74 - there is 3 different types of change if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState() In the same time it tracks "source" of the change - But! I might be wrong, but look like if your dispatches depend on props - any memoization is fucked, as long it may always produce new functions. Could be fixed
const wrapDispatchers = (mapDispatchToProps, dispatch) => {
const factory = normalize(mapDispatchToProps);// https://github.com/reactjs/react-redux/blob/master/src/connect/mapDispatchToProps.js
const dispatchs = factory();
const names = Object.keys(dispatchs);
// create forward proxy
return names.reduce( (map, key) => {
// return a function, which on-call will execute factory once more and call the real action.
map.key = (...args) => dispatch(factory(dispatch, ownProps)[key](...args)
return map;
}, {});
}
But - one could remove "areStatesEqual" at all. |
the only issue I see is this: the child component will re-render no matter what if the props changes. so it does not matter that we kept the reference returned from I'm not seeing how this is preventing re-renderings in the immediate child, perhaps in the child's children it's nice they receive the same function reference. |
Maybe I've lost a thread, but you have |
...i mean we can do anything for our own libs. im just trying to figure out what the exact thinking of it seems the "optimization" is simply that if https://github.com/reactjs/react-redux/blob/master/src/connect/selectorFactory.js#L57
So this means, when there are new props, there will be new handlers. Plain and simple. There is no optimization for that. But it could be possible if the parent updated, this: https://github.com/reactjs/react-redux/blob/master/src/connect/selectorFactory.js#L29 and did this: They are not doing this though :) Well, they/we couldn't do it generally anyway, as the child component needs those new props for its own purposes. The |
that's not to say, memoizing action creators is a good idea. lets forget about that altogether. ..im just trying to figure out what we really need to be a high quality abstraction. my question simply was this: - can we prevent re-renders by binding some props/arguments in I don't think there is any performance difference (in the general case). The wrapped component must re-render in both cases. THEREFORE, who cares about this signature: all that matters is this one: just like in the demo. if the performance gains hypothesis I just made can be disproved, we need to know. But if not, we can forget about Then we have a simple formula for |
Keep in mind - there is 2 things to solve
This means - one could try to make a better This also means that there must be some "migration stories" about how to do "something" with old react-redux, and how to do the same with a new. And why the new is better. |
i dont care about react-redux or the old ways. im building my own Framework :) the old ways are all garbage. if we followed the mainstream, we'd be composing everything until our apps collapse instead of developing far more fluidly with Redux-First Router. You're a user of that lib right? Either way, the kung fu of Reactlandia is more "authentic" than what's main stream :) and there's a lot of new things coming. I'm just trying to collate all the optimizations of Respond Framework will have components like this FYI:
no more language about "action creators." same redux functionality. the overall concept: "what if we built React with a global state store in mind from the start?" ps. RFR is about to have a big release that has been 6+ months in the making. It will be the foundation of Respond Framework.
Stage 2 is making |
So, then make a first step - extract the code from sandbox and found a new project, next to RFR. |
we still want to help the mainstream |
So you can create something more perfect for you, as long they have to maintain some compatibility. Keeping in mind, that they are just upgrading to react16/context API - the public API should be absolutely the same. but I am always keen to taste something new, something different. And, yep, trying to fix the imperfect things. |
...so basically we just gotta make sure The UI database concept for "remixx" (which is our own library you mentioned) is this: const reducers = combineReducers(allReducers) // we like normalized primary state, unlike MobX
const selectors = combineSelectors( {
derivedState1: state => state.foo[state.bar],
derivedState2: arg => state => state.foo[arg],
})
const uiDatabase = createRemixxStore(reducer, selectors, enhancer)
const router = createRouter(routesMap, options)
const { firstRoute } = createRespondApp(uiDatabase, router)
await firstRoute()
Respond.render(App, document.getElementById('root')) or: const { firstRoute } = createRespondApp(reducers, selectors, routes, options)
await firstRoute()
Respond.render(App, document.getElementById('root')) and then your components look like this: const MyComponent = (props, state, events) =>
<div>
<span>{state.derivedState1}</span>
<span>{state.derivedState2(props.arg)}</span>
<Link to={events.list({ category: 'respond-framework' })} /> // action creators generated from routesMap in RFR/Rudy :)
</div>
remixxConnect()(props => MyComponent(props.props, props.state, events)) And fyi, here's some capabilities of the routes map now: const routesMap = {
LIST: {
path: '/list/:category',
beforeThunk: ...,
saga: ...,
thunk: ...,
observable: ...,
graphql: gql`
query Videos($category: String!) {
videosByCategory($category: String!) {
name
youtubeId
}
}`
}
} So above are several concepts. The different keys on But in terms of our shared focus, So basically, we want to setup our UI database once and only once, and in our components never worry about it again. It's SQL for your UI database, where normalization is the name of the game. Selectors are like MySQL views or more commonly how rows are merged/combined through foreign keys. So similar to a database, it's a structure/schema you setup once at the beginning. Sure, there may be times where you add a
You may be wondering how the Apollo/GraphQL aspect works--simple: we copy over the denormalized queries that your components receive as props to a reducer state. And then we watch the query, and make sure references stay the same to optimize re-rendering when mutations occur. So essentially you have So back to the proxy stuff that this lib does well. Basically, the goal is to make a lazy UI database in terms of your selectors. Only when they are accessed, are they run, and watched. By "watched" i mean, now the component will re-render when it changes, similar to how selectors now are only run when attached to a live component instance. By default these selectors need to be instance-level memoized, or ideally be able to be shared as you mentioned in your "cascading" comment. A unique aspect is this one: derivedState2: arg => state => state.foo[arg] becomes this: derivedState2a: state => state.foo[arg1] // arg in closure, now we memoize based on just `state`
derivedState2b: state => state.foo[arg2] I'm not exactly sure of the implementation, but the point we need But, do we start watching for all these additional arguments forever? Or only when the corresponding component instances are alive? Of course the latter. Here's the subscription mechanism implementation (pre the new context api):
So it's an inverse of how React/Redux used to work, where each component instance listened to every store update. Instead, component instances notify the Now this concept post new context API must be a bit different. We could still do the above implementation, but apparently that would lead to "tearing" in React Async due to time slicing. I.e. components might be trying to access dead state, and error on Here's a quick way to do it off the top of my head: Basically our top level provider will broadcast to all instances. Then each instance reads the broadcasted message, and if the keys they need are in fact changed, then it updates, otherwise That's a bit more "work" that could affect perf but not much. Perhaps it has some other benefits. Another option of course is to always run all selectors on each state change. That would be unnecessary work, if only some of them are used. By taking this concept, we could simplify the above algorithm so that component instances never need to notify the store of what keys it's watching. Rather, we keep that logic all in the babel-transpiled component hoc ( The one downside that unfortunately makes this impossible is that some of our One of the overall concepts of this approach is that there is likely less comparisons, as instead of doing it on every nested component (perhaps redundantly), we we do it once at the top level. If "tearing" wasn't an issue, it would be great, because then the store directly tells individual components to update with no more comparisons at the component level. I really hope Fiber is able to broadcast these messages and let components short-circuit via
So anyway that's the overall concept of The net result is our components become templates like the old days. Except with a modern twist: they are reactive client-side beyond a single render. It's all just React. All of React capabilities is still available to you. But for the majority of your app you just emit events and respond with data (from your routesMap), and render from these params/props/vars in your components. Another core notion: it's "evented React." These templates do nothing but standard design logic + emit events. This is different than what you will be doing with apollo and what we see in a lot of apps, where you're defining queries right along side your components. In this system, these are dumb templates, and the smartest thing they do is emit/dispatch a simple event object from time to time. The "Respond Way" is to do no more than this; leave it to your It's going back to the good old days of server-side MVC. It's an architecture, whereas now we have none (just infinite composability). The achiles heal of this system is "Redux Modules"--we'll supply this feature as well via a babel plugin that essentially prefixes reducer + actionType names. You'll essentially create a folder
That key is guaranteed to be unique like mongoIds (one in a guzzilion). You don't wanna see that long key postfix in your devtools? Well, our business model is we're making our own devtools, and selling those FYI. Monthly fee $15/month, and our devtools will be 1000X better than the free one in the chromestore. ..we'll hide the postfix key if you don't want to see it :) ..among many many other features I have in mind. RFR is already the most fluid way to develop React/Redux apps. There are many use-cases it doesn't readily solve. The next version which will be out in 1-2 weeks has 75X more capabilities. It's 51% of the work that makes up the Respond Framework. Remixx is just sugar. The final sugar cube is "react-modern" which is this: const MyComponent = (props, state, events) =>
div([
MyComponent2({ foo: 123 }),
MyComponent3({ baz: 22 }, span('hello'))
]) No more JSX. No more HTML-like crap. But not via the old way of The way you teach students [from scratch] is: "Code is just a list of what you want done, like a recipe: - buy eggs
- buy milk
- mix In code that would be: buyEggs()
buyMilk()
mix() But what does "mix" do? There's multiple ways to make an egg breakfast. Well, picture a - buy eggs
- buy milk
- mix
- beat eggs
- pour milk
- fry in pan How would this look in code: buyEggs()
buyMilk()
mix()
function mix() {
beatEggs()
pourMilk()
fry()
} So functions can simply be made up of other functions. Your to do list becomes a christmas tree of function calls. As it turns out, this happens to be very similar to how web pages and essentially anything visually is displayed on screens. It's just "boxes within boxes." Function calls within function calls. This is exactly what React is made of. Therefore by learning this technique you can go from knowing zero javascript to knowing React in record timing. Lets pretend our multi-tiered to do list is in fact a plan for a painting: - Frame
- Canvas
- Horses (2 stallions)
- Barn
-Door
-Window
- Grass In React it looks almost identical: Frame(
Canvas([
Horses({ kind: stallions, number: 2 }),
Barn([ Door(), Window() ]),
Grass()
])
) As you can see, it's just specialized syntax to represent the same thing you would write on a yellow post-it note if you were trying to be extra consistent in your formatting. But more importantly, the design of apps and websites is nothing more than planning out all the tiers (called "nesting") of a painting! With one difference: digital paintings are interactive and change over time. How might we address that here. Let's imagine the horses are either in the painting or not in the painting (depending on if their owner took them out for a walk). And to simplify, let's zoom in on the "nesting" and pretend we can only see the "Canvas" level. This brings us to the "Canvas function": function Canvas(props) {
if (props.ownerOnWalk) {
return [
Barn([ Door(), Window() ]),
Grass()
]
}
return [
Horses({ kind: stallions, number: 2 }),
Barn([ Door(), Window() ]),
Grass()
]
} And we use it like so: Frame(
Canvas({ ownerOnWalk: true })
) But how do we trigger this event? function Frame(props, state) {
return [
Canvas({ ownerOnWalk: state.onWalk }),
button({ onClick: goForWalk })
]
} Where did "state" come from? State is a 2nd argument that "Respond" automatically passes for you. You only have to ever concern yourself with a single props argument.
Ok, but what about "goForWalk" and how did function Button(props, state, events) {
return button({ onClick: events.goForWalk })
} when you create your respond app, it creates both the state and events object for you and passes it to all functions in addition to props! This means your component code can be very simple, yet very capable at the same time. If you can paint a painting, you can paint a Respond/React app. Just emit events in a few places. It's as if you pointed to a part of your painting (like the barn door) and gave it magic capabilities. Anyway, I could go on and on, and start explaining reducers. This is a concept a long time in the making. It's not hyper app. It's not Elm. It's React with an MVC architecture, and some "sugar" to make it extremely accessible, while undoing the "component everything" obsession that no longer serves us. One last thing: you can do const routes = {
LIST: {
thunk: ({ state, action }) => {
state.onWalk = true
state.category = action.category
}
}
} Lastly, we will also have the alternate style of reducer: const routes = {
LIST: {
reducer: (state, action) => ({ onWalk: true, category: action.category })
}
} You can mix these with regular reducers that can only touch one slice of state. If you have both, the regular reducer "wins" because it has a stronger focus on the given state key. So that's why Remixx has 2 Xs: RemiXX Redux + MobX modules etc :) |
It took some time to read and understand all of this. From one point of view - looking promising, from another - a bit extreme. I need to see this in action. Regardless of the issue itself - I've found a way to solve it, but it will took some time to make it work on the systems without Proxies. |
Is the Proxy polyfill not a full solution? I thought we can just paste the js and it works like the Promise or fetch polyfill. To be a bit honest I was surprised it existed—I thought proxies were something that couldn’t be polyfilled. Enlighten me. |
There are 2 polyfills, one is simple - could set a trap on set/get/call. Could be done via object.defineProperty. Unfortunately, I've failed to solve this task. (for now, looking other ways) babel-plugin-transform-object-rest-spread destroys the proxy. It just gets all the props outsite "the state", and stores in another object, already not controlled by me. I could count or "all" methods as used, or all as not used. But I also could trigger on "enumeration"/spreading. |
As long there is no way to distinguish object spread and deep-equal - that's really no way to solve this.
Solution tried:
Followup - let user disable this notification, or enable only for top-level keys. |
With a given code
memoize state should not react to any state change, as it would.
Posible solutions:
Currently, memoize react on key
get
, but it could react on keyread
. This will require all objects to be wrapped by something with valueOf/toString. They key will be added to tracking if someone "read" it, or "return" it.Could affect performance. Unstable.
Track such situations and encourage a user to change the code
The text was updated successfully, but these errors were encountered: