@wordpress/data: Introduce useSelect custom hook.#15737
Conversation
|
|
||
| _Returns_ | ||
|
|
||
| - `Component`: Enhanced component with merged state data props. |
There was a problem hiding this comment.
This documentation will get replace once I add inline docs to the new withSelect. I just want to gather initial feedback on approach before doing so.
There was a problem hiding this comment.
I guess this needs an update too.
| useIsomorphicLayoutEffect( () => { | ||
| latestIsAsync.current = isAsync; | ||
| renderQueue.flush( queueContext ); | ||
| }, [ isAsync ] ); |
There was a problem hiding this comment.
Assuming the approach is sound, these could be extracted to internal only named hooks to make it a bit easier to follow in the main exported hook.
There was a problem hiding this comment.
You can include this in the previous hook:
useIsomorphicLayoutEffect( () => {
latestMapSelect.current = mapSelect;
if ( latestIsAsync.current !== isAsync ) {
latestIsAsync.current = isAsync;
renderQueue.flush( queueContext );
}
isMounted.current = true;
} );There was a problem hiding this comment.
hmm, ya I guess. I mostly liked the separation because the flush would only happen if isAsync changes (hence the depedency (so removes the need for the conditional check). So tomahtoes/tomatoes :)
There was a problem hiding this comment.
Yeah, I was just thinking it could cause some issues with the order of effect clean-up/execution, but I think it's fine.
| @@ -0,0 +1,208 @@ | |||
| /** | |||
There was a problem hiding this comment.
This file is just kept in this pull initially as a reference to compare the logic in the useSelect hook against what withSelect was doing. Once an approach is signed off on, then this can get removed.
| */ | ||
| // const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? | ||
| // useLayoutEffect : useEffect; | ||
| const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? |
There was a problem hiding this comment.
Do we really care about this? I mean even if we do (server-side usage), this feels like something that should be done across the whole codebase. I wonder if we should keep it separate and just use useLayoutEffect here.
There was a problem hiding this comment.
ya I wondered the same thing. I also tried just using useEffect and the page doesn't load at all (which I guess is to be expected). Which makes me wonder if this would even work with server side rendering.
There was a problem hiding this comment.
useEffect is a no-op in the server.
There was a problem hiding this comment.
in a7b799f I just switched to useLayoutEffect only. Redux needs to do the isomorphic effect because as a library it's used in more environments but until GB supports server side rendering generally we shouldn't implement.
| }, [ registry, nextProps ] ); | ||
|
|
||
| useIsomorphicLayoutEffect( () => { | ||
| const unsubscribe = registry.subscribe( () => { |
There was a problem hiding this comment.
I expect this will break the re-rendering order, because the children will subcribe before the parents. Right?
I think the solution might be to subscribe synchronously at the render level (but only manage to do it once).
There was a problem hiding this comment.
There was a problem hiding this comment.
hmm so the subscription callback would likely have to be a ref maybe?
There was a problem hiding this comment.
This basically means for me that subscriptions should happen at the root level of the hook and not inside a useEffect.
Something like
const registry = useRegistry();
const isAsync = useAsyncMode();
const previousRegistry = usePrevious( registry );
const previousIsAsync = usePrevious( isAsync );
if ( registry !== previousRegistry || isAsync !== previousIsAsync ) {
// Trigger subscription
}There was a problem hiding this comment.
There was a problem hiding this comment.
Do you think you'd be able to provide an example (codepen or something) where the React Async rendering would break this. Asking because that would be a good way to try and test alternative implementations addressing this issue.
There was a problem hiding this comment.
That would take a while to set up. The entire React API relies on render being idempotent. Note that the current implementation of withSelect also has this issue as it has side effects in the constructor.
From the React docs:
Conceptually, React does work in two phases:
- The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls render and then compares the result to the previous render.
- The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.) React also calls lifecycles like componentDidMount and componentDidUpdate during this phase.
The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming async mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).
Render phase lifecycles include the following class component methods:
- constructor
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- setState updater functions (the first argument)
Because the above methods might be called more than once, it’s important that they do not contain side-effects. Ignoring this rule can lead to a variety of problems, including memory leaks and invalid application state. Unfortunately, it can be difficult to detect these problems as they can often be non-deterministic.
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following methods:
- Class component constructor method
- The render method
- setState updater functions (the first argument)
- The static getDerivedStateFromProps lifecycle
Note:
This only applies to development mode. Lifecycles will not be double-invoked in production mode.
For example, consider the following code:
class TopLevelRoute extends React.Component {
constructor(props) {
super(props);
SharedApplicationState.recordEvent('ExampleComponent');
}
}At first glance, this code might not seem problematic. But if SharedApplicationState.recordEvent is not idempotent, then instantiating this component multiple times could lead to invalid application state. This sort of subtle bug might not manifest during development, or it might do so inconsistently and so be overlooked.
By intentionally double-invoking methods like the component constructor, strict mode makes patterns like this easier to spot.
There was a problem hiding this comment.
Thanks for sharing. I think we probably have a lot of components that suffer from these issues. Makes me think we don't really support async mode. Not to say that we shouldn't but if feels like something we should invest in at a more global level.
From what I understand, say we have a global counter, we could increment (side effect) from within hooks. If the render function increments this counter, we'll still be certain that children components will pick a higher-number from this counter than their parent as even if react could double invoke parents render function ... it will still call the render of the children after the parents or is this a wrong assumption (assuming initial rendering)?
There was a problem hiding this comment.
Here is the execution order:
render parent
render childA
render childB
useLayoutEffect cleanup childA
useLayoutEffect cleanup childB
useLayoutEffect cleanup parent
useLayoutEffect effect childA
useLayoutEffect effect childB
useLayoutEffect effect parent
useEffect cleanup childA
useEffect effect childA
useEffect cleanup childB
useEffect effect childB
useEffect cleanup parent
useEffect effect parent
You can't run side effects in a parent before running them in its children, because side effects only happen after the first render. This is true without hooks as well. The only alternative I see would be to stagger-render (pause render at each level of the tree to run side effects), but performance would suffer, and you would still have issues when children are rendered conditionally/dynamically.
| */ | ||
| useIsomorphicLayoutEffect( () => { | ||
| onStoreChange(); | ||
| }, [ registry, nextProps ] ); |
There was a problem hiding this comment.
onStoreChange can be the dependency here.
There was a problem hiding this comment.
It seems that this means this hooks makes the "nextProps" (the dependencies of the callback) mandatory. I'd think that we should also support a naive version where the mapping is attempted on each render if nextProps is undefined.
There was a problem hiding this comment.
It seems that this means this hooks makes the "nextProps" (the dependencies of the callback) mandatory.
undefined === undefined, so it will only run when the registry changes.
There was a problem hiding this comment.
I almost think it should be mandatory. This allows useSelect to effectively "know" the dependencies. I'm thinking then it's possible for implementations of just the hook to describe what dependencies the provided mapSelect callback has.
Thus the new withSelect hoc is provided all the props coming in from the parent as dependencies (simply because there's no way to know what mappers will be provided by the components composed with withSelect).
There was a problem hiding this comment.
I almost think it should be mandatory.
I agree with that but conceptually speaking, it behaves differently than the useEffect dependencies and could mislead people.
useEffect( something ) updates on each render, I expect useSelect( something ) to update on each render as well.
There was a problem hiding this comment.
nextProps is problematic, because it's an object so you put the burden on the user to memoize it.
onStoreChange doesn't need to be memoized. It can just read refs.
nextProps should be an array that gets passed as the dependency array to the useIsomorphicLayoutEffect of this chat thread.
There was a problem hiding this comment.
You also don't want to run this during the first render, so this should come before the effect that sets isMounted, and it should check for it to be true.
There was a problem hiding this comment.
There was another comment attached to an outdated code snippet about assuming nextProps is an array and spreading it here (thus being more consistent with dependency shape with useEffect). I think that's probably what we should do here and have withSelect do the necessary implementation to follow that signature expectation for useSelect.
There was a problem hiding this comment.
Yeah that's better, but it still means the component re-renders twice. First when nextProps change and then after the selector is run.
Take a look at this approach: https://github.com/reduxjs/react-redux/blob/v7-hooks-alpha/src/hooks/useSelector.js
Here:
https://github.com/reduxjs/react-redux/blob/0e41eaeebf5d8d123daaa50a91ee1c219c4830de/src/hooks/useSelector.js#L61-L64
You would also check if nextProps have changed.
There was a problem hiding this comment.
This actually removed the need for nextProps.
I updated my gist to implement all the behavior you want. Look at how we can implement withSelect now.
https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630
| unsubscribe(); | ||
| renderQueue.flush( queueContext ); | ||
| }; | ||
| }, [ registry ] ); |
There was a problem hiding this comment.
onStoreChange is also a dependency here.
There was a problem hiding this comment.
Hmm, there is risk of that causing more subscription re-renders though right?
There was a problem hiding this comment.
I don't think onStoreChange should be memoized. It can be a ref if you also need to use it in the other effect.
See #15737 (comment)
| @@ -0,0 +1,25 @@ | |||
| /** | |||
| setMapOutput( newMapOutput ); | ||
| } | ||
| } | ||
| }, [ registry, nextProps ] ); |
There was a problem hiding this comment.
I feel this should be ...nextProps and not nextProps
|
I gave this some more thought now that we want the callback to run again when It makes more sense for the callback to be called again when it changes. This was needed regardless, because the callback might have new closures every time it changes and the component wouldn't reflect that until the next store update. Now, users can just memoize their callback if they don't want it to run on every render, using a traditional hook dependency array. This is what I did in my implementation of See the updated gist: https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630. I also added the queue context and flushing that @nerrad added in this PR. You can copy and paste this into this PR if you don't see any issues with it. |
nerrad
left a comment
There was a problem hiding this comment.
Thanks again @epiqueras for the code example. It's looking more and more like the redux useSelect implementation :)
This appears to work in rough testing and I mostly implemented the code from your gist with an exception wrt to the memoized mapSelect callback in the withSelect hoc (see my comments).
| * Fallback to useEffect for server rendered components because currently React | ||
| * throws a warning when using useLayoutEffect in that environment. | ||
| */ | ||
| const useIsomorphicLayoutEffect = |
There was a problem hiding this comment.
I re-implemented this. I realize that GB as a whole isn't really supporting server rendering but with this being a separate package that could be used in a server rendering environment, this might be a valid thing to do (and follows a pattern that Redux is using for their hooks)
| ownProps, | ||
| registry | ||
| ), | ||
| [ ownProps ] |
There was a problem hiding this comment.
Note, I could not do Object.values( ownProps ) here because of this error:
Warning: The final argument passed to useCallback changed size between renders. The order and size of this array must remain constant.
I experimented with generating a consistent array of values from the prop keys in the same order over multiple renders (and filling missing values with undefined) but this caused the same error (because it looks like the number of props can increase between re-renders as well). This almost smells like a problem with implementation in some component in the editor tree but I'm uncertain what to do here. On the surface just passing in the prop object seems to be sufficient for this case.
There was a problem hiding this comment.
ownProps changes on every render so this is like not memoizing it and running the callback every time.
I think we can just wrap the inner component in a React.memo.
Updated: https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630#file-with-select-js
There was a problem hiding this comment.
ownProps changes on every render so this is like not memoizing it and running the callback every time.
I couldn't implement React.memo because it resulted in invalid blocks on existing content. I'm not sure of the internal workings of it but it's a no go based on results. So I implemented pure which should work similarly.
I'm not seeing much difference in behaviour between the two when I do a rough comparison using the react dev profiler tool:
However pure does seem marginally better so probably good to use it.
There was a problem hiding this comment.
memo doesn't implement shouldComponentUpdate like pure does, it just adds a special flag for the renderer to handle.
Both have the same functionality, but internally React has different rules for when to ignore a shouldComponentUpdate or a memo.
It sounds like these blocks are relying on some indeterministic and undocumented behavior.
There was a problem hiding this comment.
The blocks throwing the invalidation issue were basic blocks included with GB (paragraph block for instance).
There was a problem hiding this comment.
Could there be a state/props mutation somewhere in there?
|
@youknowriad there's still some failing e2e tests in here, it's unclear to me what the issue is with the tests (because of my lack of familiarity with them) but I picked a few of the tests that I can replicate in testing myself (such as re-enabling nux tips) and I'm not reproducing the e2e fails. |
|
For reference, here are the failing e2e test:
|
| useIsomorphicLayoutEffect( () => { | ||
| const onStoreChange = () => { | ||
| if ( isMounted.current ) { | ||
| const newMapOutput = latestMapSelect.current( registry.select, registry ); |
There was a problem hiding this comment.
So So if I'm reading this properly, the mapping function is never called when the mapping function changes or when the props used in the mapping function changes. This feels like a bug to me, what if a prop changes and the store wasn't updated in the meantime.
I guess what I'm saying here is I'm still not sure why we dropped the dependencies array from the arguments.
There was a problem hiding this comment.
So if I'm reading this properly, the mapping function is never called when the mapping function changes
I think it is:
if ( latestMapSelect.current !== mapSelect ) {latestMapSelect gets set to the current mapping function on ever render and the next render compares that value with the previous. If they differ, then the function is invoked again to get the mapOutput.
I guess what I'm saying here is I'm still not sure why we dropped the dependencies array from the arguments.
The dependency is now on the mapSelect callback. This gives more control to developers to optimize on their mapSelect function.
I think there's two approaches we could go here:
useSelect requires a dependency array and uses that to optimize rendering.
Disadvantages:
withSelectwill be harder to optimize for becauseownPropsfluctuates too much so we can't just doObject.values( ownProps )- Easier for developers to shoot themselves in the foot because their dependency array doesn't correctly all the dependencies in their
mapSelectcallback (granted that might be somewhat resolveable via an ES lit rule).
Advantages:
- enforces optimal approaches for performance.
- makes the dependencies more clear and obvious.
Developers do their own memoization of the mapSelect callback.
Disadvantages:
- performance responsibility is shifted to developers, so they can still shoot themselves in the foot wrt performance (granted, something being non performant via not working are too different things :) )
Advantages:
- More flexibility for developers with regards to how they optimize things.
withSelectis a bit more easier to optimize (although there's still some difficulties as noted in the comments for my latest commit).
There was a problem hiding this comment.
I have a strong preference for having a dependencies argument in the signature, mimic useEffect, and absorb the optimizations in the framework personally.
Will we need more fine-tuning? Maybe, but it seems like this could be a useSelectAdvanced similar to how there's a connectAdvanced in react-redux
There was a problem hiding this comment.
I think the connector's job is to update the component when it has to update. Memoizing a selector for performance is a separate concern.
Keep in mind that a lot of the time, specially for computationally expensive selectors, they will already be defined elsewhere without closures over props, removing the need for inline hook callback memoization. This codebase is a good example of that.
I also like the fact that the public API remains simple and developers have one less thing to learn. They already have to learn how to use primitive memoization hooks, so we can build on that.
Let's look at a few examples:
// Callback never changes.
// Performance is good.
useSelect( importedSelector );
// Callback changes and re-runs on every render.
// `.getItem` is memoized.
// Performance is good.
useSelect( ( select ) => select( 'store' ).getItem() );
// Callback changes and re-runs on every render.
// `.getItem` is memoized with `prop`.
// Performance is good.
useSelect( ( select ) => select( 'store' ).getItem( prop ) );
// Callback changes and re-runs on every render.
// `.getItem` is not memoized.
// Performance suffers.
// We memoize ourselves.
useSelect( useCallback( ( select ) => select( 'store' ).getItem( prop ), [ prop ] ) );
// Callback changes and re-runs on every render.
// `.getItem` is not memoized.
// Performance suffers.
// We delegate memoization to the implementation.
useSelect( ( select ) => select( 'store' ).getItem( prop ), [ prop ] );See where the two approaches differ in the last two calls. My main issue with having the dependencies array is that the user has to learn a new interpretation of it. Does it memoize the callback everywhere? Only for renders? Does it always use the latest one in store updates? Does it update the memoized value after that?
It very quickly makes it much harder to think about and learn. Developers already know how useCallback works and it's much easier to say: This hook calls your callback every time it changes and when the store updates, and then returns its output. If your callback is expensive, you can memoize it, because it's only called when it changes or when the store updates.
There was a problem hiding this comment.
I think the connector's job is to update the component when it has to update. Memoizing a selector for performance is a separate concern.
The same thing could have been said about useEffect hook. Why the React team decided to implement the dependencies there and not rely on useCallback to ensure it runs the effect only when the callback changes?
I understand that a "pure" implementation is better for an advanced developper that understands how to override the default behavior and build on top of it but let's take a look at the common use-case from a user's perspective:
const { block } = useSelect( ( select ) => {
return {
block: select( 'core/editor' ).getBlock()
// call other selectors
};
} );
In our previous attempts to improve the performance of the editor, the fact that the selectors were memoized didn't prove to be enough to address the editor performance issues once we load a lot of blocks. The problem was that a lot of selectors were being run (memoized or not) on each subscribe. So for me, the more we avoid running selectors entirely, the better is.
I understand that I'm basing this on previous metrics, that might not transpose 1-1 to the current use-cases. Ideally, we'd perform performance tests and compare.
My main issue with having the dependencies array is that the user has to learn a new interpretation of it
That's my main issue with the dependencies-less implementation as well. Users need to learn how to memoize things by them selves (using other hooks) while the dependencies array is a common pattern already used in useEffect.
Ultimately, I think we should take a path, mark it as experimental and evaluate it with real usage.
There was a problem hiding this comment.
In thinking about this more. It seems that the behavior of both approaches is the same if we omit passing any second argument and if we don't check for the callback refrence (always run the callback on render and rely on memoization), that suggests that we could potentially adopt a first version without any attempt to memoize (based on callback or based on dependencies) as a common ground and adapt using concrete measures.
There was a problem hiding this comment.
The functionality of useEffect is completely different. It's not just about closures. There you might want to run the effect more or less times for other reasons. That's why they didn't rely on useCallback.
This is really just about:
useSelect( mapSelect, [ dep ] )vs.
useSelect( useCallback( mapSelect, [ dep ] ) )The first one does have less code and I guess it's fine if we document it well enough. Something like:
This hook calls your callback every time it changes and when the store updates, and then returns its output. If your callback is expensive, you can pass a dependency array as the second parameter to memoize it, because we will only call it when it changes or when the store updates.
I updated the gist: https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630.
|
Looks like the e2e tests all pass on the latest push to this branch 🎉 so as far as I can tell here's some remaining points to iron out:
Anything else? |
The current implementation of
|
See discussions and changes here: #15737 (comment).
It does suffer from it. It subscribes in constructors. These are called top-down parent to children, but not when children are rendered dynamically. I.e. lists, conditionals, switches, code-splitting, etc. The only way to solve this is for subscriptions to have a sense of location in the tree. This can be done by wrapping every subscribed component in a new context provider that overrides the subscription for its entire sub-tree. This is what I do think the @nerrad I've updated the gist to handle this: https://gist.github.com/epiqueras/7eae39ba6b903286cf17a4907902a630 |
That's a good point 👍 I guess the fact that we didn't notice might indicate that it's harder to trigger but you're right. |
|
@epiqueras Do you think the "try/catch" approach could work for us even if our selectors are not entirely pure? this was suggested as something that would break the approach in the document you shared. If that's the case, I'd be happy to move forward with that proposal. |
A selector's corresponding resolvers will either get triggered in the first or a second call to I think they were more worried about a side effect that depended on props running with stale props and then somehow affecting the next render. This is not the case for how resolvers work in |
|
Thanks again @epiqueras for the explanation and the code examples. I'll try and get another commit up sometime later today. |
|
In 585795c I've implemented the try/catch approach as outlined by @epiqueras which is also similar to what Redux is doing in their useSelect hook. Note, I still am unable to use Which is found in the block api: gutenberg/packages/blocks/src/api/registration.js Lines 115 to 119 in 5c34f2f This error happens for both core blocks (in this case For now I'm using |
|
Looks like all e2e tests pass on the latest build here as well so if this approach looks like what we want to go with in the initial iteration I'll get to work on updating unit tests and adding unit test coverage for this along with doing up the necessary CHANGELOG.md updates etc. |
585795c to
c8c2ca8
Compare
|
woops looks like I missed pushing a commit (see c8c2ca8). @epiqueras this is a bit different than your gist because your gist had some bugs. |
|
I still don't think any of the last pushes are sufficient as I think there's still scenarios where the incoming select hasn't changed but the dependencies has and the previous mapOutput will still be returned. So it's unclear to me yet how the |
|
Alright with the latest commits this is ready for final review. Please note the following changes though: A
|
youknowriad
left a comment
There was a problem hiding this comment.
Awesome work on this PR @nerrad I think we can ship it and iterate on it as we expand usage. useDispatch? :)
Yup I plan on working on that over the course of this week 👍 |
packages/data/src/index.js
Outdated
| import * as plugins from './plugins'; | ||
|
|
||
| export { default as withSelect } from './components/with-select'; | ||
| export { withSelect } from './components/with-select'; |
There was a problem hiding this comment.
Was there a reason this was changed from a default to a named export?
There was a problem hiding this comment.
I think you're commenting on an early iteration of this file (I was temporarily leaving the old withSelect in place for a reference point).
There was a problem hiding this comment.
I think you're commenting on an early iteration of this file (I was temporarily leaving the old
withSelectin place for a reference point).
I must have had that stored from an earlier, unsubmitted review 🤔 I didn't write it today.
| import useAsyncMode from '../async-mode-provider/use-async-mode'; | ||
|
|
||
| /** | ||
| * Favor useLayoutEffect to ensure the store subscription callback always has |
There was a problem hiding this comment.
I really admire the emphasis in documentation in the changes of this pull request. 👍
| * In general, this custom React hook follows the | ||
| * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). | ||
| * | ||
| * @param {Function} _mapSelect Function called on every state change. The |
There was a problem hiding this comment.
Minor: Considering that this is enshrined in the public-facing documentation, I think we could have optimized for this to be the mapSelect, either choosing _mapSelect or (preferably, if one exists) a better name for the internal reference.
I guess it depends on your preference for or against "modifying" the argument, but since the arguments aren't const, you could always re-define:
export default function useSelect( mapSelect, deps ) {
mapSelect = useCallback( mapSelect, deps );| const registry = useRegistry(); | ||
| const isAsync = useAsyncMode(); | ||
| const queueContext = useMemo( () => ( { queue: true } ), [ registry ] ); | ||
| const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); |
There was a problem hiding this comment.
Who will be the first to breach 9007199254740991 (Number.MAX_SAFE_INTEGEER) useSelect renders in a page session? 😆
There was a problem hiding this comment.
lol if that happens, there will be other problems likely ;)
Ya it's possible it will because the subscription no longer gets set on construct (and is only set on effect - and unsubscribes on either a registry change or unmount). Worth testing to confirm but very likely will fix. |
| } ); | ||
|
|
||
| return () => { | ||
| isMounted.current = false; |
There was a problem hiding this comment.
I don't know that it ends up being an issue, but this callback won't only be called for unmounting, it will also be called on a change in registry (and, if #19205 lands, in change in isAsync). I guess it would depend on the order of how onStoreChange or a subsequent call to the previous useIsomorphicLayoutEffect which sets isMounted.current back to true. My worry could be that onStoreChange wouldn't run the selector if it considers the component unmounted. I guess for the purpose of the selector running as unique to the registry (or isAsync in #19205), we don't want the invalidated change callback, and we can feel confident that either renderQueue.add or onStoreChange would be called for the next registry? In which case, it's more an issue of a confusing name.
There was a problem hiding this comment.
I guess for the purpose of the selector running as unique to the registry (or isAsync in #19205), we don't want the invalidated change callback, and we can feel confident that either renderQueue.add or onStoreChange would be called for the next registry?
Exactly
In which case, it's more an issue of a confusing name.
I am not sure what else to call it. isCleaningUp, isUnsubscribing?
There was a problem hiding this comment.
In which case, it's more an issue of a confusing name.
I am not sure what else to call it.
isCleaningUp,isUnsubscribing?
Yeah, either of those would work pretty well I think (flipping the boolean value).
There was a problem hiding this comment.
That boolean covers both scenarios^^
|
Hey folks, is there a recipe to use |
|
@zanona Hooks cannot be used in class components. This is a limitation imposed by React, and not something we have control over. In my experience, most any component can be ported to or expressed exclusively as a function component with hooks, so it's worth considering to migrate or write new components this way. Otherwise, |
|
A workaround for using react hooks with class components if they are super complex and hard to convert immediately to a function component is to implement the hook in a higher order component that wraps your class component. However, in this specific case, that's already handled for you because |


Note: For latest wrap up comments on this pull see also this comment
Description
See #15473 for background and #15512 for initial experimental approach to the new useSelect. The initial iteration of this approach used some code provided by @epiqueras (see comment, but did require modification. In this approach to useSelect:
The signature is
useSelect( mapSelect: function, deps: array )mapSelectreceives theregistry.selectfunction as the first argument, andregistryas the second. This, follows roughly the signature of themapSelectToPropssignature withSelect currently exposes (so this preserves that api). ThemapSelectdoes not receive the "ownProps" because in most cases of just hook usage, the callback provided will internally be working with props. I'm on the fence with this because it does put more burden on implementors to account for possible stale prop usage in their callbacks but this is mitigated for most cases by the factuseSelectalways invokes the latest mapSelect provided on render (via usage ofuseRef).depsshould an array of values used for memoizing the provided mapSelect (similar in behaviour to react hooks with dependencies). Internally, the newwithSelectwraps the internal component usinguseSelectwithpure.Other
useRegistryhook is exported and use exported context objects ( internally only - no need to export the entire context with the exposure of the hook).withSelectexport which eliminates a lot of code and allows us to measure behaviour across all of GB in one swoop.withSelectfile for reference temporarily in this pull, but it will get removed if not used.How has this been tested?
Currently I've just tested playing around with blocks in the editor and generally looking for breakage. Things to watch for will be:
Types of changes
These are mostly internal changes so it's not expected to be a breaking change. The current signature for
mapSelectToPropsis preserved on implementations ofwithSelectso no effects there.However, this does impact a lot of code (because of the
withSelectimplementation of the new hook). So the potential for regressions is significant here.Next Steps:
Checklist: