Closed markerikson closed 5 years ago
I think a dependencies array is the obvious way to deal with props, itβs a well known pattern in hooks land and minimal effort, especially with the lint rule. Redux is all about being more explicit over being most succinct anyway.
Needing to wrap components to use hooks is obviously extremely unintuitive. Do none of the other libs have a better solution to this issue? Since it only is needed to handle mixing and matching connect and hooks, there should be a way to opt out for projects that will not mix and match, then it sounds like a workable intermediate workaround.
Did some reading on the v7 code too and came to pretty much to the same conclusion as @ricokahler: It doesn't seem like it's possible to implement hooks without some kind of wrapping component unless React itself would come forward and give us something like
My hooks implementation is indeed way too naive.
My hooks implementation is indeed way too naive.
@epeli If it makes you feel better, I have an even more naive useRedux
implementation in prod right now lol
I agree with everyone that the hooks still requiring the HOC is annoying/gross but I think in practice, it won't change how people use the redux hooks. See this example pasted from my previous post:
// this custom hook isn't aware of `enableReduxHooks` and everything is fine
function useTodo(todoId) {
const todo = useRedux(state => state.todos[todoId], [todoId]);
return todo;
}
const Todo({ id }) {
const todo = useTodo(id);
return <div>{/* ... */}</div>
}
export default enableReduxHooks(Todo);
I would also like to figure out a way to do it without a wrapper but I don't think it's possible.
For those who haven't read the code: the only reason we need the wrapper is to add that subscription layer to enable top-down updates. See here from the v7 branch: https://github.com/reduxjs/react-redux/blob/79982c912d03c55ac7059b61806a1387352e91f3/src/components/connectAdvanced.js#L387-L394
This Provider
propagates the Subscription
instances to the next component wrapped in connect
:
By the nature of HOCs, it's easy to wrap the component with another provider seamlessly but with hooks, there is no way to do that π
After I figured that out, I admitted defeat and started seriously thinking about how to implement useRedux
(aka useMapState
, useSelector
) with the constraint of the wrapper and it's actually kind of nice from an implementation standpoint.
Earlier in the thread, @Jessidhia brought up the same API as useRedux
just named useSelector
and she said:
[The
useSelector
API] would also have the side-effect of creating a separate subscription peruseSelector
, though. I don't know if that's a relevant performance consideration or not.
With the wrapper, instead of having one redux subscription per useRedux
(aka useSelector
) call, it would be one redux subscription per enableReduxHooks
call.
Internally the enableReduxHooks
wrapper could create one subscription and manage pushes to each useRedux
call inside that "subscription layer" (where each enableReduxHooks
call is one layer).
I think this would bring performance on par to how connect
works now because this would make it work like how connect works.
It's unfortunate and gross that the HOC is still needed but with this constraint there is a clear path to fully function redux hooks that work alongside with connect
π (in my mind at least)
Maybe I'm not thinking outside of the box enough but if we can figure out how to get the tiered subscriptions (e.g., the "order matters") without the wrapper then we can ditch the HOC.
@ricokahler your posts reflect my own thoughts almost exactly. I also got to the conclusion that currently the HOC will still be required. Instead of enableReduxHooks
my idea was to just call it connectHooks
to stay in line with existing naming.
I think this code (basically your example with some inlining) is quite concise and readable.
export const Todo = connectHooks(({ id }) => {
const todo = useRedux(state => state.todos[id], [id]);
return <div>{/* ... */}</div>
})
Regarding the alternative approach of "enforcing" that no props are used for selecting the state (which would only be possible with linters as far as I can see), I think it would definitely be worthwhile to do some performance tests to see how big the impact of those additional renders really is. I have some free time on my hands the next couple of weeks, so I may try my hand at it.
Lastly, I also plan to take a deep dive into the fiber implementation. Maybe I'll find some hidden nugget that would allow us to have a proper hooks-only implementation.
Giving up the possibility of filtering state with props is not an option in my opinion. Maybe for some, but for some cases it's going to cause a huge amount of unnecessary renders.
I think the hybrid approach (HOC + hooks) is the way to go for now. The main issue with the HOC isn't the HOC itself IMO, but the code clutter and ergonomics. And, if in the future there's a HOC-less solution, the migration path will be almost pain-free, and the HOC can be updated to a NOOP.
I really don't like the idea of having to use a HOC just to use hooks.
Unfortunately I do agree that it looks like it may be a necessity atm, but I would really like to try to come up with another option.
The vague notion I'm tossing around in my head is if there's some way we can construct our own in-memory tree structure in <Provider>
that mirrors the shape of the store subscriptions in the React tree, so that we can trigger updates in the appropriate sequence. Problem is, we don't actually know where each component is in the tree.
If y'all have any ideas on how to pull off such a feat, I'm all ears.
@saboya One of the suggestions above was something like the following code, which would put some burden on the library users to always remember to use useMemo
, but should keep performance problems minimal due to not everything being re-rendered (again, only tests with concrete numbers will verify or contradict this).
export function Todo({ id }) {
const todos = useRedux(state => state.todos);
const todo = todos[id]
return useMemo(() => (
<div>{/* ... */}</div>
), [todo])
})
@MrWolfZ : the question of how many components render is only part of the issue.
In that snippet, yes, the child components memoized by the useMemo()
would only re-render when todo
changes, because React will bail out when it sees the ===
elements in this spot as last time.
However, the Todo
component itself will still have to render even if todo
hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.
Now, we ought to try building these options out, and then updating the benchmarks suite to have some hook scenarios for comparison with our existing HOC scenarios. (No idea how that will work out).
I don't think using dependencies to useRedux
is a good idea, suppose we use redux like:
const User = ({id}) => {
const user = useRedux(
state => state.users[id],
[id]
);
return <div>{user.name}</div>;
};
It looks very concise and readable by specifying id
as a dependency to mapState
function, however this implies a fact that the selector won't change around renders, which is not a stable consumption:
const User = ({userType, id}) => {
const selectUser = userType === 'admin'
? state => state.admins[id]
: state => state.users[id];
const user = useRedux(selectUser, [id]);
return <div>{user.name}</div>;
};
User
won't get correct result from redux if userType
is changed without id
change, and lint rules like exhaustive-deps
is not able to detect the fact that selectUser
actually depends on userType
variable.
We have to tell developers to modify this example code to:
const User = ({userType, id}) => {
const selectUser = state => userType === 'admin' ? state.admins[id] : state.users[id];
const user = useRedux(selectUser, [userType, id]);
return <div>{user.name}</div>;
};
This may be OK in a self controlled app but is not the case as a general purpose library like react-redux
, we should try to satisfy all kinds of developers without having implicit restrictions.
I'd prefer to manage my selector via useCallback
myself:
const User = ({userType, id}) => {
const selectUser = useCallback(
userType === 'admin'
? state => state.admins[id]
: state => state.users[id],
[userType, id]
);
const user = useRedux(selectUser);
return <div>{user.name}</div>;
};
This is more straightforward and is able to utilize exhaustive-deps
lint rules
Unfortunately I do agree that it looks like it may be a necessity atm, but I would really like to try to come up with another option.
@markerikson a babel plugin? idk
last time I checked. create-react-app didn't play nicely with those though
Edit: there are these things called babel macros that work with create-react-app. I'm not sure what they do exactly but maybe we can babel some HOCs in magically.
Edit edit: In order to use babel macros, the importing line has to match /[./]macro(\.js)?$/
.
Is that cool or gross? import useRedux from 'react-redux/macro'
is gross.
Eh that's about as far as I'm willing to go for considering babeling enableReduxHooks
.
@otakustay this is a bad example. Just like with any hook, functions also need to be specified as dependencies, so if you pass selectUser as a reference, then you need to specify it as a dependency and the exhaustive-deps rule would also capture that missing dependency on the function in useMemo or useCallback. In your example id is actually not a dependency of that hook but just the function is. See https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies and also https://overreacted.io/a-complete-guide-to-useeffect/
@MrLoh
Just like with any hook, functions also need to be specified as dependencies
Except useCallback
. What I'm discussing is actually "should useRedux
includes the capability of useCallback
", as a result I said NO, we should just keep selector function as a dependency of useRedux
@MrWolfZ
Maybe you already took this into account, when writing your suggestion. If so, I apologize.
Thanks for the sample code. I noticed React-Redux
's Subscription
implementation but I think we can avoid that by connecting redux via state
π
My statement all props are up
was talking about one component.
I think why the zombie child component
is unintuitive & really need a fix is because that's the situation where data of the SAME component are consistent.
i.e. the stale props (props.id) and state (todos[props.id]) of the same component could be not synced (at a point in time)
The case of your sample code is two components' internal state could be not synced at the same time.
I think we probably shouldn't need to worry about this case too much as you probably hardly see it in a real-life example? --- If you subscribe to the same state, there would be no point of passing the state via props. And if you don't pass the state data to the child component via props, it will be no chance that two copies of states exist in the SAME component.
@markerikson Regarding this comment
@MrWolfZ : the question of how many components render is only part of the issue.
In that snippet, yes, the child components memoized by the
useMemo()
would only re-render whentodo
changes, because React will bail out when it sees the===
elements in this spot as last time.However, the
Todo
component itself will still have to render even iftodo
hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.Now, we ought to try building these options out, and then updating the benchmarks suite to have some hook scenarios for comparison with our existing HOC scenarios. (No idea how that will work out).
I found an interesting statement in the official react dooks regarding useReducer having a similar behaviour. Let me quote it here:
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldnβt be a concern because React wonβt unnecessarily go βdeeperβ into the tree. If youβre doing expensive calculations while rendering, you can optimize them with useMemo.
So they seem to think that having that single component re-render without any children also re-rendering shouldn't be too much of an issue.
That said, I completely agree that both API designs I posted are ugly and should be prevented if possible.
@t83714 if you look at the example @markerikson gives for the zombie child problem, you will find a pattern that is likely to occur in real applications (otherwise, how would they have noticed the zombie child problem in the first place?).
Sorry for the spam, but just for completeness' sake, here my proposed hooks implementation that requires neither a HOC nor use of useMemo
and does not suffer from zombie children, but has the downside of calling the state selector with inconsistent props and also calling it on each render and twice for each relevant store change. You can see it in action here with the zombie child example from @markerikson. Of course it is not fully performance optimized yet and I am sure I am missing some edge cases, but this should demonstrate the idea of just ignoring errors during the subscription callback.
import { ReactReduxContext } from "react-redux";
import { useContext, useReducer, useRef, useEffect } from "react";
export function useRedux(selector, deps) {
const { store } = useContext(ReactReduxContext);
const [_, forceRender] = useReducer(s => s + 1, 0);
const latestSelector = useRef(selector);
const selectedState = selector(store.getState());
const latestSelectedState = useRef(selectedState);
useEffect(() => {
latestSelector.current = selector;
}, deps);
useEffect(
() => {
function checkForUpdates() {
const storeState = store.getState();
try {
const newSelectedState = latestSelector.current(storeState);
if (newSelectedState === latestSelectedState.current) {
return;
}
latestSelectedState.current = newSelectedState;
} catch {
// we ignore all errors here, since when the component
// is re-rendered, the selector is called again, and
// will throw again, if neither props nor store state
// changed
}
forceRender();
}
checkForUpdates();
return store.subscribe(checkForUpdates);
},
[store]
);
return selectedState;
}
@MrWolfZ
Thanks for the example link. I used a much simple array based sample app to reproduce the problem π
I think you're right. As long as we set up separate subscriptions for components, it's unavoidable unless we use batchUpdates
or custom subscription logic to enforce top-down updates
π
useStoreSelector((state, [a, b]) => selectSome(state, a + b), [a, b])
.How critical is it to be able to use component props as part of your state extraction logic?
Here are my two cents on this ^ discussion point:
@markerikson said
However, the Todo component itself will still have to render even if todo hasn't changed, and it's the aspect of React getting involved at all that's ultimately a perf issue.
I agree with this. If you have 100 or so connected components in a list and all 100 of those update, that's still 100 updates regardless if those updates are fast or not. I acknowledge that you can useMemo
to speed up all 100 of those updates⦠but it's still 100 updates.
For example, if you have 100 <Todo />
s in a list and you allow the user to edit the title of the todo using an <input />
, then you will feel the 100 updates with every keystroke. I've been in that situation before and I've learned my lesson. From my experience regarding components, it's never a good idea to turn an O(1) problem into an O(n) problem if you don't have to.
However, @gaearon in this facebook incubator issue has also gave the notion of an API that doesn't take in props.
Here is a full quote of his post:
To be clear I'm not suggesting this particular API. Just that the notion of running a selector that involves props to determine a bailout has a lot of pitfalls (such as handling stale props), and can cause other issues in concurrent mode (such as the current code which mutates refs during rendering). It doesn't really map to the React conceptual model nicely. From React's point of view, props don't really exist until you render β but this Hook is trying to "bail out" at arbitrary times outside rendering.
One alternative design could be to allow selectors but only static ones:
import {createSelector} from 'react-redux-hook' const useTodosLength = createSelector(state => state.allTodos.length) function Todos() { const todosLength = useTodosLength() return <h1>Total: {todosLength}</h1> }
If you need to select based on a prop value, you can do this with a component layer:
import {createSelector} from 'react-redux-hook' const useTodos = createSelector(state => state.allTodos) function Todos() { const todos = useTodos() return todos.map(todo => <Todo {...todo} />) } const Todo = React.memo(() => { // ... }) Essentially, in this case the selecting component is your mapStateToProps. :-)
His post echos what @t83714 had said previously just lifting the selector out of the actually hook.
From an API standpoint, the static creatorSelector
makes sense if you don't want to include props but in general, I still think using props to select a specific piece of state is necessary to make things scale.
BUT theory is theory. Until we build it out and benchmark it, there's no way to know. Maybe something like createSelector
is the way to go? I'm skeptical but always open to different ideas (e.g. I'm still thinking about babeling that HOC in).
I finally have some numbers for y'all. I implemented two alternatives for useRedux
which you can see here. Alternative 1 is my proposed implementation from above with some slight adjustments. Alternative 2 is the approach of a static selector and useMemo
.
To benchmark this I have created a copy of the deeptree benchmark for each alternative and made some minor adjustments for each alternative. For alternative 1 I replaced ConnectedCounter
with this:
const Counter = ({ idx }) => {
const value = useReduxAlternative1(s => s.counters[idx], [idx])
return <div>Value: {value}</div>
};
For alternative 2 I replaced ConnectedCounter
with this:
const selector = s => s.counters;
const Counter = ({ idx }) => {
const counters = useReduxAlternative2(selector)
const value = counters[idx]
return useMemo(() => <div>Value: {value}</div>, [value])
};
Below you can find the results. Assuming I have correctly implemented everything, alternative 1 is on par with 7.0.0-beta.0. However, alternative 2 is massively slower (much slower than I expected).
@markerikson Which of the benchmarks do you think would be best suited to test the hooks implementations? I don't really want to re-implement all of them for all alternatives, so it would be great to know which your preferred one is (e.g. because it reflects real world usage best).
Results for benchmark deeptree:
ββββββββββββββββ¬ββββββββββ¬βββββββββββββββ¬ββββββββββββ¬ββββββββββββ¬βββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Version β Avg FPS β Render β Scripting β Rendering β Painting β FPS Values β
β β β (Mount, Avg) β β β β β
ββββββββββββββββΌββββββββββΌβββββββββββββββΌββββββββββββΌββββββββββββΌβββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 5.1.1 β 14.55 β 109.9, 0.1 β 6466.00 β 8835.47 β 3001.32 β 14,15,14,15,14,15,14,15,14,15,14,15,14,15,14,14 β
ββββββββββββββββΌββββββββββΌβββββββββββββββΌββββββββββββΌββββββββββββΌβββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 7.0.0-beta.0 β 24.11 β 111.8, 0.8 β 337.77 β 13320.03 β 4784.63 β 24,25,24,23,24,25,24,24 β
ββββββββββββββββ΄ββββββββββ΄βββββββββββββββ΄ββββββββββββ΄ββββββββββββ΄βββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββ
Results for benchmark deeptree-useReduxAlternative1:
βββββββββββββββ¬ββββββββββ¬βββββββββββββββ¬ββββββββββββ¬ββββββββββββ¬βββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β Version β Avg FPS β Render β Scripting β Rendering β Painting β FPS Values β
β β β (Mount, Avg) β β β β β
βββββββββββββββΌββββββββββΌβββββββββββββββΌββββββββββββΌββββββββββββΌβββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ€
β 7.0.0-hooks β 24.05 β 111.7, 0.7 β 536.28 β 13591.96 β 4872.18 β 23,22,23,24,23,24,23,22,24,25,24,23,24,25,25 β
βββββββββββββββ΄ββββββββββ΄βββββββββββββββ΄ββββββββββββ΄ββββββββββββ΄βββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββ
Results for benchmark deeptree-useReduxAlternative2:
βββββββββββββββ¬ββββββββββ¬βββββββββββββββ¬ββββββββββββ¬ββββββββββββ¬βββββββββββ¬βββββββββββββ
β Version β Avg FPS β Render β Scripting β Rendering β Painting β FPS Values β
β β β (Mount, Avg) β β β β β
βββββββββββββββΌββββββββββΌβββββββββββββββΌββββββββββββΌββββββββββββΌβββββββββββΌβββββββββββββ€
β 7.0.0-hooks β 7.96 β 104.6, 4.6 β 15053.94 β 4996.25 β 1724.94 β 8,7,8,8 β
βββββββββββββββ΄ββββββββββ΄βββββββββββββββ΄ββββββββββββ΄ββββββββββββ΄βββββββββββ΄βββββββββββββ
Update: got some great review feedback from @MrWolfZ - I'm definitely on the drawing board still. But really happy to have this issue known to me now. Shall feedback with any updates as and when.
Hi all, joining this discussion as @MrWolfZ informed me that my library would suffer the same "zombie child" issue as discussed here.
I've managed to get around this by lifting the redux store subscriptions for the hooks to the root context, which allows me to execute the mapStates in an order that is more likely to represent the component hierarchy.
PR is here: https://github.com/ctrlplusb/easy-peasy/pull/137
Demo: https://codesandbox.io/s/pm39mzn13m
I still need to do a lot more tinkering. But wanted to put this on your radar. Hoping to get some early feedback on whether this is a crazy idea or not. π
Here is a very preliminary proof of concept branch with the HOC + hooks and here is a forked demo of the zombie child with the connect hooks
Don't take the code too seriously, I didn't spend much time on it (or test it). The only important thing here is the idea. I think we all knew that something like this would work but here is something a little more concrete.
Here's what it's doing (or at least trying to do):
connectHooks
, creates a "subscription layer" where each call to useRedux
will subscribe to a "hooksManager
" and then the hooksManager
will propagate to each useRedux
hook in the "layer"useRedux
subscriber in the layer is batchedCheckout react-hooks-easy-redux by @dai-shi. It seems to me it dodges these issues entirely. The api in it is simply
const state = useReduxState();
No map state etc. It uses proxies to monitor what parts of the state is used in a given component and just forces rerender for the component if a monitored part of the state changes. So any state mapping you need to do you can do directly in the render. So in this case batchedUpdates should be enough to prevent issues with stale props?
At least it passes this test https://codesandbox.io/s/2p4kxnxj1j
Here's some more discussions at https://github.com/dai-shi/react-hooks-easy-redux/issues/10
Not 100% sure how performant this solution is.
Also obvious downside of it is the requirement for Proxies so IE11 support can be a challenge.
An even simpler API and it doesn't suffer from inconsistent props? What a gem.
Also obvious downside of it is the requirement for Proxies so IE11 support can be a challenge.
Unfortunately, I have to support IE 11 :( . Since we have the properties we want to proxy from the store, we can iterate through them and then use Object.defineProperty
for the proxy mechanism instead of ES6 proxies.
Yeah, I think immer does it like that if there's no Proxy support https://github.com/mweststrate/immer#immer-on-older-javascript-environments
If I understand correctly another downside in react-hooks-easy-redux is that you cannot bail rendering if you only use derived state to produce output.
Ex.
function HasComments(props) {
const state = useReduxState();
const hasComments = state.posts[props.postId].comments.length > 0;
return hasComments ? "yes" : "no";
}
This component would always render when comment count changes. Not just when it goes from 0 to something else.
@epeli not with the fork of redux i'm working on where selectors are created at store creation time: createStore(reducer, selectors, etc)
.
See:
@epeli I should note that react-hooks-easy-redux also has issues with Concurrent Mode, since it uses side effects in render (by attaching side effects to property accesses).
react-hooks-easy-redux also has issues with Concurrent Mode, since it uses side effects in render
It should work in Concurrnt Mode. It only updates refs in commit phase.
@dai-shi I really like the elegance of the proxy approach. Here are my two cents on the pros and cons:
pros:
useReduxState
call per component while with alternative approaches people may use multiple hook invocations which will in turn create multiple subscriptions etc.cons:
Object.keys(state.items)
)ref.current
inside effects but e.g. trapped.reset()
etc. during render mutate the objects inside those references); that said, while this means it is not strictly pure, all mutations your library currently performs are "predictable" and reproducible, so the render still has the pure property of same inputs = same outputsOne way to solve the intermediary value accesses without needing a redux fork or similar is to still use an API which passes the selector to the hook, but then uses proxies to trace property accesses inside the selector to determine equality. This would alleviate the stale props issue since something like items[id].prop
will not throw an error if the item with id
does not exist anymore.
Final note regarding concurrent mode: As @markerikson noted in a source code comment, I believe as well that any solution which accesses store.getState()
during render will have issues with concurrent mode, which includes your library.
@MrWolfZ Thanks for your attention to the library and your detailed pros/cons! I will refer your comment in my issues for further discussion.
@MrWolfZ
it makes render impure since through the proxy
That's not so much unlike the react-redux hoc which is impure as a parent component doing the work. There's gonna be impurity somewhere until React builds a global state store into React itself (and passes it down to all as a 2nd argument to all components), and it's very clear they have gone the opposite direction of Redux (probably because its lack of modularity).
One way to solve the intermediary value accesses without needing a redux fork or similar is to still use an API which passes the selector to the hook, but then uses proxies to trace property accesses inside the selector to determine equality
@epeli was saying that the closure is the real problem. So if he's correct and if the selectors weren't defined in the component function, or at least didn't enclose any variables, and rather the variables were passed separately, I think you're right, and selectors could work without a fork! Eg:
const hasComments = (state, postId) => {
return state.posts[postId].comments.length > 0
}
const MyComponent = props => {
const deps = [props.postId]
const state = useReduxState(hasComments, deps)
return <div>{hasComments ? 'yes' : 'no'</div>
}
That should work, as there is no difference between passing hasComments
there or on store creation.
lot of good brainstorming happening around @dai-shi's library! (and not to mention @theKashey's lib: proxyequal which is the basis for all this!!
Lately, I'm designing an experimental selector based hook in my project https://github.com/dai-shi/reactive-react-redux (the project name changed.) I ended up with developing something similar to what @MrWolfZ proposed in https://github.com/reduxjs/react-redux/issues/1179#issuecomment-476105825 with Proxy.
If Proxy-based approach is still immature for react-redux v7, and I suppose so, @MrWolfZ 's hook would be good for the official react-redux hook as it keeps the same mental model. Just my two cents.
Yeah, unfortunately, I don't think I can justify using anything that relies on Proxies completely atm. I ran a poll the other day, and it looks like 50% of React-Redux users are still targeting ES5 environments: https://twitter.com/acemarke/status/1111050066532265984 .
Not all proxy features are needed. And what is needed can be addressed under the hood (aka polyfilled), so that IE11 can work with no different work required of the end user. Proxy support isnβt in fact a barrier.
To be clear, not all of proxy features can be pollyfilled. But all thatβs needed for this solution can be polyfilled.
https://github.com/GoogleChrome/proxy-polyfill - only ownKeys
trap is not supported
@faceyspacey @theKashey there is one edge case where the polyfill doesn't work: if you have an optional property on the root state and add that property later on it will not notice this new property. Here is a reproduction. In this example, if you click "Add property", it will not cause Child
to re-render.
However, this issue is very unlikely to surface in a real application, since it only happens for optional properties on the root state. For more nested properties it will simply be less performant, since it will stop the access detection chain at the parent of the optional property and then re-render Child
whenever the parent changes. Here is an example showing this. In this example, the render count will not increase when clicking "Add property 2" without the polyfill but will increase with the polyfill.
(You can force the polyfill to be used even in modern browsers by just adding a <script>delete Proxy</script>
to public/index.html
's <head/>
.)
@Jessidhia thank you for the hint. I tried deleting the global proxy object inside index.js but couldn't get it to work properly. With your suggestion it works perfectly, so I adjusted the sandboxes.
@MrWolfZ - the example is awesome - short and sound. And I have no idea why it's not working.
So - if you are accessing some prop, which does not exist - the access would not be recorded, thus the changes in that prop would be ignored. That might be a big issue, actually, @ctrlplusb already created the same issue, but with another way to reproduce. I am afraid, but any change in object keys should be considered as a major update. And, huh, that would cause some performance implications.
@MrWolfZ : "optional properties on the root state" sounds exactly like a typical "dynamically loaded code-split reducers" scenario.
I don't see any technical way to solve this case without proxies, but there is a non-technical way.
replaceReducer
later, or ask to use Object.keys
and mark a selector as dependent o keys, and once the got changed - it would be recalculated.Honestly - the first variant is more acceptable.
I've spent the last few hours doing research. I've re-read #1063 and this issue, put together a list of existing unofficial Redux hooks libs, looked at some of the code snippets pasted in these threads, and glanced through the source of reactive-react-redux
and proxyequal
.
I don't have any grand insights or solutions atm - I'm still trying to absorb a lot of info.
My immediate random thoughts, in no particular order:
The best of the existing unofficial hooks libs is easily https://github.com/facebookincubator/redux-react-hook , both in terms of actual popularity and how much thought has gone into it. Runners-up:
Pros and cons of the most promising approaches I've seen so far for hooks implementations that try to handle the "stale props / zombie child" issues:
connect
reactive-react-redux
lib:Interestingly, the "Zombie Child" demo I'd created that fails in v7-alpha.5
actually appears to work okay with v7-beta.1
. Not sure what specifically changed to make that happen.
There were mentions of needing to put some kind of a "intermediate HOC" in the tree to act as a sort of store update propagation layer. I think that connect()
itself would serve that purpose, particularly with the v7 implementation. Right now, I'm pretty sure that connect()
overrides the Subscription
in context even if it doesn't subscribe to the store itself. (Which, come to think of it, might not even be the behavior we want?)
If we somehow had a way to track subscriptions in correct nested order, without relying on overriding a context value, I think that would resolve most of the issues we're discussing around stale props and stuff. That said, I truly don't know if that's even possible, given that we don't know where a given component is in the tree when it tries to subscribe.
Definitely punting on Concurrent Mode worries for now. Honestly, all things considered, I don't think the Redux core ever will be truly CM-compatible. That said, we still can't say much for sure until CM is actually out.
Following on from my earlier poll that showed 50% of React-Redux users still target ES5, I asked about whether it's okay for a new hooks API to be ES6+ only. 62% say yes, it is. I've also got a poll up asking who should be responsible for polyfilling, the library or the end user, and early results are 65% say the user.
So here's a hypothetical suggested path towards actually building this:
useSelect()
: subscribe-onlyuseActions()
: action creators onlyuseDispatch()
: just dispatchuseStore()
: get the store if you really need ituseRedux()
: roughly equivalent to connect()
nowuseTrackedSelect()
or something like that that would be based on the work from reactive-react-redux
.
connect()
and even useSelect()
in an ES5-only if you want. proxyequal
is a few K by itself. I wouldn't want to force folks to pull that in unless they're using useTrackedSelect()
. Would need to figure out how to get that set up well for tree-shaking.Thoughts?
Yeah. I have one Thought, not related to this topic, but better be answered here.
In short - I have no idea how and why people are ok with redux. Unless you are an experienced developer with quite a special mindset, it's quite hard to create a proper boilerplate around selection from state and derived data creating in mapStateToProps
.
To be clear - the problem is not with redux
, the problem is with memoization
it requires to live happily ever after without cascade updates.
Reselect
is fragile, and not component-architecture compatible by design. re-reselect
had never been an option for me. React.useMemo
could be a great solution, but it has to live within React render cycle, which affects API.
I've tried my best - kashe as a solution for derived data generation/memoization and memoize-state (proxyequal) to remove than engeneering aspect from memoization.
So - my thoughts are simple - we need not a hook-based API, but an API there is would be or harder to make a mistake, or easier to write more sound code.
99% redux customers are doing it wrong, not knowing/checking what they are doing, or where/why memoization fails. Every time I convince someone to install why-did-you-update-redux and actually test "quality" of their "redux" - it's π€―. Using custom equality check to perform deep comparison in reselect or areMergedPropsEqual
- easy. Not using any memoization at all - even easier. Babel optional chaining (returning a new "empty" object every time) - a new trend.
A generic hook solution, like redux-react-hook
is good, and looks quite similar to the "old" redux, but useMappedState
like a forcing you to continue use hooks, especially useMemo, inside mapStateToProps
, while you cant. It's +10 WTF per minute.
What if _first try to create an addition to react-dev-tools(aka reselect-tools), to help you build/visualize selectors according to your factual state and factual usage (dev tools could track both), and then build hooks API__ in a way to easier utilize results of such tool.
PS: your concerns about proxyequal size are not quite correct. It's a low-level library which does not handle edge cases. Memoize-state, which build on top of it to handle those cases is twice bigger.
@theKashey amen, preach!
Basically, the only way the bell curve of developers (according to skill level) will ever be able to build properly rendering apps is through a proxy-based connectless future. Having to configure connect
is also an absolute waste of developer time, given there is a path forward where we don't need it. connect
will inevitably die, and the sooner the better.
I've been talkin about this for 2 years. It's time to level up the Redux world. I personally have chosen to focus on other things, FYI, because this is inevitable. It's like how Marc Andreesen talks about industries like the music industry (and now many others) inevitably being toppled: "technology is like water; it inevitably fills all the gaps and there's nothing we can do to stop it; we're better off supporting the natural direction of growth." That's not the exact quote, but you get the idea: we're best off facilitating a proxy-based connectless future.
So the most important thing the Redux ecosystem needs isn't a "hooks" API. HOCs still work beautifully. What needs to be determined is if hooks truly provide much value here (which they don't) or if we're just doing things because the React team opened up a new shiney API.
The thing is this:
mapStatesToProps
is needed)If we want to "be like water" (this time in the Bruce Lee way), and hit a few birds with one stone, react-redux 8 should be hooks + proxies. 7 remains for everyone that can't support proxies yet.
Lastly, there are workarounds for supporting proxies. Namely what MobX always did where keys must be known upfront, as @theKashey mentioned. So in 8.5, perhaps that's introduced so even IE11 can make use of the transparent proxy-based approach. @theKashey has a few ideas how to handle this. But obsessing over it is not the path of the people that actually get this done. Build it first without supporting the non-proxy approach, and then circle back, like the Vue team is doing with Vue 3:
yes, out the gate, Vue 3 will not support IE11 and browsers that dont support Proxies.
In conclusion, the best use of available energies is toward helping @dai-shi and @theKashey optimize their proxy-based approach in their 2 libs:
They are doing something truly groundbreaking. I usually don't poke my head out in these threads--so the whole purpose of my post here is to make it known how important their work is. The other hooks approaches are all following traditional approaches. Where's the revolutionary spirit? That's the whole point of software after all.
There's some remaining perf issues, but it's clearly achievable. What those 2 have done has proved a lot already. The perf for a great number of cases is comparable to react-redux 7. The one that's sub-par is a contrived case. It needs to be addressed regardless. The point is: we're close, and would be closer if more from the community got serious about this approach, rather than treating it like some far off thing. Of course current react-redux users are tied to IE11. But it's a totally different story for apps starting today. Not offering a futuristic approach, and not making that the main focus, will only mean Redux falls more and more out of favor. Bold action is required. Long live Redux!
@theKashey , @faceyspacey : fwiw, I definitely appreciate the work you and @dai-shi are doing investigating various Proxy-based approaches, especially given that I don't have time to try any of that myself.
That said, given React-Redux's "official" status, and the fact that it's one of the single most widely used libraries in the React ecosystem, we have to carefully balance a lot of different concerns: API design, browser compatibility, familiarity, teachability, community usages, versioning, and more. That means we do have to move a bit slowly and favor a conservative approach overall.
There's a very clear interest from the community in having us add some kind of hooks-based API, hence this issue. connect()
is extremely well known, and there's a straight path from an API like mapState
to useSelect()
or similar. Adding something like useSelect()
now doesn't preclude us from adding more APIs down the road.
But, anything we add now does have to continue to work and be supported going forwards. I refuse to put out new public APIs without ensuring they're tested and rock-solid, and I'm not going to force our users to keep bumping major versions (or even worse, break their code) because we did something bad. I feel bad enough about the v6 -> v7 change as it is - I'm the one who pushed for the v6 context-based state propagation. Granted, at the time it really did look like the way to go, but the churn here is basically my fault for choosing an approach that didn't pan out.
I agree that IE11 is (slowly) on the way out, and that use of Proxies is only going to increase over time. I think there's some great possibilities around using them, particularly for React-Redux. But, I'm not ready to jump that chasm yet.
I'd encourage you to continue your experiments and to work out the rough edges around these approaches. I've already been keeping an eye on what y'all have been doing, and I'm excited to see how it all turns out. When the time comes to try pulling some of those ideas into React-Redux itself, I will gladly learn from what you've done, give all of you full credit for any inspiration we take, and ask you to help contribute if possible.
Until then, the most straightforward path to getting something out the door is to build on the hooks-based code we've got in v7 right now, and provide hooks APIs that closely resemble use of connect()
.
Mark, I updated my post, make sure to check out what Vue 3 is doing where they wont initially support proxies:
https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf
I wouldn't be closed off to a react-redux going a similar route sooner than it seems you're currently considering. Vue after all is the whole framework, whereas react-redux is just one choice of many libs you can use with React.
There's 3 ways react-redux could get serious about it sooner:
Basically it would be nice if react-redux got serious about supporting this endeavor sooner than later. Right now @dai-shi and @theKashey need a lot more eyeballs than yours and mine.
Everyone else, HELP WANTED HERE:
We need some established pattern for redux
, and right now it does exist. Then we could try to create an eslint
plugin, like react-team created for hooks
(really - that's the deal breaker) and mitigate the majority "π"s.
So - let's focus not on the implementation details, but on the sound API, it would be possible to name as a pattern and create an infrastructure around.
My personal opinion - I love to separate concerns as much as possible (ie Smart/Dump) to make tests/storybooks easier. Right now all hooks realization are tightly coupled, they just form one big component, and does not answer this call.
So - Hooks vs Tests: the engage.
So the most important thing the Redux ecosystem needs isn't a "hooks" API. HOCs still work beautifully. What needs to be determined is if hooks truly provide much value here (which they don't) or if we're just doing things because the React team opened up a new shiney API.
@faceyspacey I don't necessarily agree with this. I care a lot about React hooks and a redux API because it simplifies composing together and reusing reactive dependency injectors (i.e. things that provide data + have the ability to case a re-render). Historically, I've been composing reactive dependency injectors via HOCs and that made the rest of my team scratch their heads e.g.
const Container = compose(
withRouter,// get the `location` from react-router
connect(/* ... */), // use the location + redux state to derive props
lifecycle({
componentDidUpdate() {
// watch for changes in those props and refetch if necessary
}
}),
)(PresentationalComponent);
export default Container;
with hooks, composing reactive dependency injectors looks much better/is more readable and useEffect
is particular helpful
export default function Container() {
const { location } = useRouter();
const exampleState = useRedux(/* ... */);
const calculatedProp = useMemo(() => {
return // ...
}, [location, exampleState]);
useEffect(() => {
// refetch data or similar
}, [calculatedProp]);
return <PresentationalComponent exampleProp={calculatedProp} />;
}
I think the above is a very "react" way of addressing the concerns @theKashey has with redux memoization. Don't get me wrong, @theKashey is 100% correct with the pains of memoization but hooks with useMemo
might also be a good way to deal with it??
It's not fool proof but at least better than the current state of composing HOCs.
I like @markerikson proposed possible path forward simply because it gives me hooks and it's great incremental approach to improved patterns.
hooks with useMemo might also be a good way to deal with it??
That's a problem! They could not! 99% redux-hook implementation uses subscription mechanics to react to store changes and skip unnecessary updates, so your useMemo
would be called by redux
out of React rendering cycle. "WTF!", you can't use hooks in redux-hooks API, which is a bit controversial.
Let's use this thread to discuss actual design considerations for an actual hooks API.
Prior references:
Along those lines, I've collated a spreadsheet listing ~30 different unofficial
useRedux
-type hooks libraries.update
I've posted a summary of my current thoughts and a possible path forward.