-
-
Save epiqueras/7eae39ba6b903286cf17a4907902a630 to your computer and use it in GitHub Desktop.
/** | |
* WordPress dependencies | |
*/ | |
import { useContext } from '@wordpress/element'; | |
/** | |
* Internal dependencies | |
*/ | |
import { Context } from '../components/async-mode-provider'; | |
export default function useAsyncMode() { | |
return useContext( Context ); | |
} |
/** | |
* Internal dependencies | |
*/ | |
import useRegistry from './use-registry'; | |
export default function useDispatch() { | |
return useRegistry().dispatch; | |
} |
/** | |
* WordPress dependencies | |
*/ | |
import { useContext } from '@wordpress/element'; | |
/** | |
* Internal dependencies | |
*/ | |
import { Context } from '../components/registry-provider'; | |
export default function useRegistry() { | |
return useContext( Context ); | |
} |
/** | |
* WordPress dependencies | |
*/ | |
import { createQueue } from '@wordpress/priority-queue'; | |
import { useLayoutEffect, useEffect, useCallback, useMemo, useReducer, useRef } from '@wordpress/element'; | |
import { isShallowEqualObjects } from '@wordpress/is-shallow-equal'; | |
/** | |
* Internal dependencies | |
*/ | |
import useRegistry from './use-registry'; | |
import useAsyncMode from './use-async-mode'; | |
const useIsomorphicLayoutEffect = | |
typeof window !== 'undefined' ? useLayoutEffect : useEffect; | |
const renderQueue = createQueue(); | |
export default function useSelect( _mapSelect, deps ) { | |
const mapSelect = useCallback( _mapSelect, deps ); | |
const registry = useRegistry(); | |
const isAsync = useAsyncMode(); | |
const queueContext = useMemo( () => ( { queue: true } ), [ registry ] ); | |
const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); | |
const latestMapSelect = useRef(); | |
const latestIsAsync = useRef( isAsync ); | |
const latestMapOutput = useRef(); | |
const latestMapOutputError = useRef(); | |
const isMounted = useRef(); | |
let mapOutput; | |
try { | |
if ( latestMapSelect.current !== mapSelect || latestMapOutputError.current ) { | |
mapOutput = mapSelect( registry.select, registry ); | |
} else { | |
mapOutput = latestMapOutput.current; | |
} | |
} catch ( err ) { | |
let errorMessage = `An error occured while running \`mapSelect\`: ${ | |
err.message | |
}.`; | |
if ( latestMapOutputError.current ) { | |
errorMessage += `\nThe error may be correlated with this previous error:\n${ | |
latestMapOutputError.current.stack | |
}\n\nOriginal stack trace:`; | |
} | |
throw new Error( errorMessage ); | |
} | |
useIsomorphicLayoutEffect( () => { | |
latestMapSelect.current = mapSelect; | |
if ( latestIsAsync.current !== isAsync ) { | |
latestIsAsync.current = isAsync; | |
renderQueue.flush( queueContext ); | |
} | |
latestMapOutput.current = mapOutput; | |
latestMapOutputError.current = undefined; | |
isMounted.current = true; | |
} ); | |
useIsomorphicLayoutEffect( () => { | |
const onStoreChange = () => { | |
if ( isMounted.current ) { | |
try { | |
const newMapOutput = latestMapSelect.current( registry.select, registry ); | |
if ( isShallowEqualObjects( latestMapOutput.current, newMapOutput ) ) { | |
return; | |
} | |
latestMapOutput.current = newMapOutput; | |
} catch ( err ) { | |
latestMapOutputError.current = err; | |
} | |
forceRender( {} ); | |
} | |
}; | |
const unsubscribe = registry.subscribe( () => { | |
if ( latestIsAsync.current ) { | |
renderQueue.add( queueContext, onStoreChange ); | |
} else { | |
onStoreChange(); | |
} | |
} ); | |
return () => { | |
isMounted.current = false; | |
unsubscribe(); | |
renderQueue.flush( queueContext ); | |
}; | |
}, [ registry ] ); | |
return mapOutput; | |
} |
/** | |
* WordPress dependencies | |
*/ | |
import { createHigherOrderComponent } from '@wordpress/compose'; | |
import { memo } from '@wordpress/element'; | |
/** | |
* Internal dependencies | |
*/ | |
import useSelect from '../use-select'; | |
const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( | |
( WrappedComponent ) => memo( ( ownProps ) => { | |
const mapSelect = ( select, registry ) => mapSelectToProps( | |
select, | |
ownProps, | |
registry | |
); | |
const mergeProps = useSelect( mapSelect ); | |
return <WrappedComponent { ...ownProps } { ...mergeProps } />; | |
} ), | |
'withSelect' | |
); | |
export default withSelect; |
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
useEffect
is non-blocking.
useLayoutEffect
runs synchronously, before the browser has a chance to paint or anything.
If a store update happens before useEffect
is called for the latest render, it can call the callback with stale values.
Does that make more sense?
I see you implemented something similar to what react-redux did with
useIsomorphicLayoutEffect
. It's unclear to me why we wouldn't just useuseEffect
always instead, but I know the comment redux has gives some explanation:https://github.com/reduxjs/react-redux/blob/a787aeeb5ab064e66ff168eedbb962d3397b378d/src/hooks/useSelector.js#L7-L16