Could we do away with most redux concepts that we use today, and instead rely on React hooks?
This is an audacious proposal directly building up on findings from 26692. Not much thought given to cost/benefit analysis yet but the amount of work involved is potentially huge. Even if this ends up not being pursued, having a discussion would be beneficial anyway. Let's talk!
Our redux actions and selectors have complex needs such as invoking side-effects or actions and selectors from other stores. To enable these interactions, we have concepts like:
- Registry selectors
- yield anotherAction()
- Async actions, controls, thunk middleware
At the same time, we do all this things in React components using:
- useSelect
- useDispatch
- useEffect and promises
I wonder - what are the advantages of using the former over the latter? These ideas seem to be interchangeable, only using both means more code paths to maintain plus twice the learning and a higher barrier of entry. Let’s see how React hooks could replace the existing concepts side by side:
Actions with yield
These two seem to be equivalent and interchangeable:
export function* undo() {
const undoEdit = yield controls.select( 'core', 'getUndoEdit' );
if ( ! undoEdit ) {
return;
}
yield {
type: 'EDIT_ENTITY_RECORD',
...undoEdit,
meta: {
isUndo: true,
},
};
}
export function useUndo() {
const undoEdit = useSelect( select => select('core').getUndoEdit() );
const { editEntityRecord } = useDispatch( 'core' );
const undo = useCallback(() => {
if( ! undoEdit ) {
return;
}
editEntityRecord( {
...undoEdit,
meta: {
isUndo: true
}
} );
}, [ undoEdit ]);
return undo;
}
Only with the latter one doesn’t need to know about generators, controls, or dispatching actions inline.
Controls
Similarly, controls seems to exist just to handle the async behavior and grant access to different stores. Consider the yield controls.select( 'core', 'getUndoEdit' ); part of the undo action above. This is the underlying select control:
// controls.js:
export const builtinControls = {
[ SELECT ]: createRegistryControl(
( registry ) => ( { storeKey, selectorName, args } ) =>
registry.select( storeKey )[ selectorName ]( ...args )
),
// ...
}
With hooks, this control wouldn’t be required at all - its role would be fulfilled by useSelect(). The same goes for controls.dispatch.
Async controls
How about async controls, for example apiFetch? I find this:
export function* deleteEntityRecord( kind, name, recordId, query ) {
/* ... */
deletedRecord = yield apiFetch( {
path,
method: ‚DELETE’,
} );
/* ... */
To be the same as this:
export function useDeleteEntityRecord() {
/* ... */
return useCallback(async ( kind, name, recordId, query ) => {
/* ... */
await apiFetch( {
path,
method: ‚DELETE’,
} );
/* ... */
});
}
In this case yield turned out to really be just a reimplementation of await - we „hack” generators to simulate async behavior. Actions invoke other actions that are really controls, a middleware (?) calls the control handler, and then the handler waits for the actual promise. Why not remove the indirection and await for the promise directly?
Registry selectors
This registry control:
// component.js:
const postContent = useSelect(
( select ) => select( 'core/editor' ).getEditedPostContent(),
[]
);
// selectors.js:
export const getEditedPostContent = createRegistrySelector(
( select ) => ( state ) => {
const postId = getCurrentPostId( state );
const postType = getCurrentPostType( state );
const record = select( 'core' ).getEditedEntityRecord(
'postType',
postType,
postId
);
// ...
}
);
Seems to be just this hook in disguise:
// component.js:
const postContent = useEditedPostContent();
// hooks.js
export const useEditedPostContent = () => {
const { postId, postType, record } = useSelect( select => {
const postId = select('core/editor').getCurrentPostId();
const postType = select('core/editor').getCurrentPostType();
const record = select( 'core' ).getEditedEntityRecord(
'postType',
postType,
postId
);
return { postId, postType, record };
});
// ...
} );
Resolvers
Resolvers are particularly interesting. I didn’t quite figure out all the code paths that are running in order to make them work just yet, but it seems like this controls plays an important role:
[ RESOLVE_SELECT ]: createRegistryControl(
( registry ) => ( { storeKey, selectorName, args } ) => {
const method = registry.select( storeKey )[ selectorName ]
.hasResolver
? '__experimentalResolveSelect'
: 'select';
return registry[ method ]( storeKey )[ selectorName ]( ...args );
}
)
I imagine that without controls, a canonical useSelect hook could be used to explicitly trigger the resolution when needed.
Public API
One downside of react hooks is that one could no longer call wp.data.select( 'x' ).myFunc() as easily. Or is it true? This isn't fully fleshed out yet, but I imagine exploring "bridge layer" could yield a solution:
const PublicApiCompat = memo(() => {
useSelect(select => window.wp.data.select = select);
})
Other considerations
@wordpress/redux-routine would go away entirely.
- At least some redux middlewares would go away.
- Concepts of async actions, controls, registry selectors would all go away.
- All that’s left would be pure reducers, pure selectors, and pure action creators. Other than that, the logic would live in hooks.
- The code would be more approachable by new contributors.
- Using hooks would improve debugging. Currently with async generators stack traces aren’t always informative plus stepping through the code is challenging.
- A nice side-effect of this would be that it would remove the concept of registry selectors and thus make store-specific selectors possible.
- It would break BC big time, unless of course we can build a „compatibility layer” to keep the existing API in place but really call the new one under the hood.
Anything else?
Am I missing anything? Is there anything that makes this a blunder or a technical impossibility? Really curious to learn what does everyone think. cc @noisysocks @draganescu @youknowriad @mtias @talldan @tellthemachines @gziolo @nerrad @jorgefilipecosta @ellatrix @kevin940726 @azaozz @aduth @nosolosw
Could we do away with most redux concepts that we use today, and instead rely on React hooks?
This is an audacious proposal directly building up on findings from 26692. Not much thought given to cost/benefit analysis yet but the amount of work involved is potentially huge. Even if this ends up not being pursued, having a discussion would be beneficial anyway. Let's talk!
Our redux actions and selectors have complex needs such as invoking side-effects or actions and selectors from other stores. To enable these interactions, we have concepts like:
At the same time, we do all this things in React components using:
I wonder - what are the advantages of using the former over the latter? These ideas seem to be interchangeable, only using both means more code paths to maintain plus twice the learning and a higher barrier of entry. Let’s see how React hooks could replace the existing concepts side by side:
Actions with yield
These two seem to be equivalent and interchangeable:
Only with the latter one doesn’t need to know about generators, controls, or dispatching actions inline.
Controls
Similarly, controls seems to exist just to handle the async behavior and grant access to different stores. Consider the
yield controls.select( 'core', 'getUndoEdit' );part of the undo action above. This is the underlying select control:With hooks, this control wouldn’t be required at all - its role would be fulfilled by
useSelect(). The same goes forcontrols.dispatch.Async controls
How about async controls, for example
apiFetch? I find this:To be the same as this:
In this case
yieldturned out to really be just a reimplementation ofawait- we „hack” generators to simulate async behavior. Actions invoke other actions that are really controls, a middleware (?) calls the control handler, and then the handler waits for the actual promise. Why not remove the indirection andawaitfor the promise directly?Registry selectors
This registry control:
Seems to be just this hook in disguise:
Resolvers
Resolvers are particularly interesting. I didn’t quite figure out all the code paths that are running in order to make them work just yet, but it seems like this controls plays an important role:
I imagine that without controls, a canonical
useSelecthook could be used to explicitly trigger the resolution when needed.Public API
One downside of react hooks is that one could no longer call
wp.data.select( 'x' ).myFunc()as easily. Or is it true? This isn't fully fleshed out yet, but I imagine exploring "bridge layer" could yield a solution:Other considerations
@wordpress/redux-routinewould go away entirely.Anything else?
Am I missing anything? Is there anything that makes this a blunder or a technical impossibility? Really curious to learn what does everyone think. cc @noisysocks @draganescu @youknowriad @mtias @talldan @tellthemachines @gziolo @nerrad @jorgefilipecosta @ellatrix @kevin940726 @azaozz @aduth @nosolosw