Skip to content

Instantly share code, notes, and snippets.

@epiqueras
Last active May 22, 2019 01:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save epiqueras/7eae39ba6b903286cf17a4907902a630 to your computer and use it in GitHub Desktop.
Save epiqueras/7eae39ba6b903286cf17a4907902a630 to your computer and use it in GitHub Desktop.
`@wordpress/data` Hooks Prototype
/**
* 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;
@nerrad
Copy link

nerrad commented May 14, 2019

I see you implemented something similar to what react-redux did with useIsomorphicLayoutEffect. It's unclear to me why we wouldn't just use useEffect 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

@epiqueras
Copy link
Author

// 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment