khaihihi
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export * from './use-query-state';
|
||||
export * from './use-shallow-equal';
|
||||
export * from './use-store-products';
|
||||
export * from './use-collection';
|
||||
export * from './use-collection-header';
|
||||
export * from './use-collection-data';
|
||||
export * from './use-previous';
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
import { Component as ReactComponent } from '@wordpress/element';
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCollection } from '../use-collection';
|
||||
|
||||
jest.mock( '@woocommerce/block-data', () => ( {
|
||||
__esModule: true,
|
||||
COLLECTIONS_STORE_KEY: 'test/store',
|
||||
} ) );
|
||||
|
||||
class TestErrorBoundary extends ReactComponent {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = { hasError: false, error: {} };
|
||||
}
|
||||
static getDerivedStateFromError( error ) {
|
||||
// Update state so the next render will show the fallback UI.
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
render() {
|
||||
if ( this.state.hasError ) {
|
||||
return <div error={ this.state.error } />;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'useCollection', () => {
|
||||
let registry, mocks, renderer;
|
||||
const getProps = ( testRenderer ) => {
|
||||
const { results, isLoading } = testRenderer.root.findByType(
|
||||
'div'
|
||||
).props;
|
||||
return {
|
||||
results,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
const getWrappedComponents = ( Component, props ) => (
|
||||
<RegistryProvider value={ registry }>
|
||||
<TestErrorBoundary>
|
||||
<Component { ...props } />
|
||||
</TestErrorBoundary>
|
||||
</RegistryProvider>
|
||||
);
|
||||
|
||||
const getTestComponent = () => ( { options } ) => {
|
||||
const items = useCollection( options );
|
||||
return <div { ...items } />;
|
||||
};
|
||||
|
||||
const setUpMocks = () => {
|
||||
mocks = {
|
||||
selectors: {
|
||||
getCollection: jest
|
||||
.fn()
|
||||
.mockImplementation( () => ( { foo: 'bar' } ) ),
|
||||
hasFinishedResolution: jest.fn().mockReturnValue( true ),
|
||||
},
|
||||
};
|
||||
registry.registerStore( storeKey, {
|
||||
reducer: () => ( {} ),
|
||||
selectors: mocks.selectors,
|
||||
} );
|
||||
};
|
||||
|
||||
beforeEach( () => {
|
||||
registry = createRegistry();
|
||||
mocks = {};
|
||||
renderer = null;
|
||||
setUpMocks();
|
||||
} );
|
||||
it(
|
||||
'should throw an error if an options object is provided without ' +
|
||||
'a namespace property',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
resourceName: 'products',
|
||||
query: { bar: 'foo' },
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.error.message ).toMatch( /options object/ );
|
||||
expect( console ).toHaveErrored( /your React components:/ );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should throw an error if an options object is provided without ' +
|
||||
'a resourceName property',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
query: { bar: 'foo' },
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.error.message ).toMatch( /options object/ );
|
||||
expect( console ).toHaveErrored( /your React components:/ );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should return expected behaviour for equivalent query on props ' +
|
||||
'across renders',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
query: { bar: 'foo' },
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results } = getProps( renderer );
|
||||
// rerender
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
query: { bar: 'foo' },
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
// re-render should result in same products object because although
|
||||
// query-state is a different instance, it's still equivalent.
|
||||
const { results: newResults } = getProps( renderer );
|
||||
expect( newResults ).toBe( results );
|
||||
// now let's change the query passed through to verify new object
|
||||
// is created.
|
||||
// remember this won't actually change the results because the mock
|
||||
// selector is returning an equivalent object when it is called,
|
||||
// however it SHOULD be a new object instance.
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
query: { foo: 'bar' },
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results: resultsVerification } = getProps( renderer );
|
||||
expect( resultsVerification ).not.toBe( results );
|
||||
expect( resultsVerification ).toEqual( results );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
it(
|
||||
'should return expected behaviour for equivalent resourceValues on' +
|
||||
' props across renders',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
resourceValues: [ 10, 20 ],
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results } = getProps( renderer );
|
||||
// rerender
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
resourceValues: [ 10, 20 ],
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
// re-render should result in same products object because although
|
||||
// query-state is a different instance, it's still equivalent.
|
||||
const { results: newResults } = getProps( renderer );
|
||||
expect( newResults ).toBe( results );
|
||||
// now let's change the query passed through to verify new object
|
||||
// is created.
|
||||
// remember this won't actually change the results because the mock
|
||||
// selector is returning an equivalent object when it is called,
|
||||
// however it SHOULD be a new object instance.
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
resourceValues: [ 20, 10 ],
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results: resultsVerification } = getProps( renderer );
|
||||
expect( resultsVerification ).not.toBe( results );
|
||||
expect( resultsVerification ).toEqual( results );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
it( 'should return previous query results if `shouldSelect` is false', () => {
|
||||
mocks.selectors.getCollection.mockImplementation(
|
||||
( state, ...args ) => {
|
||||
return args;
|
||||
}
|
||||
);
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'products',
|
||||
resourceValues: [ 10, 20 ],
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results } = getProps( renderer );
|
||||
// rerender but with shouldSelect to false
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'productsb',
|
||||
resourceValues: [ 10, 30 ],
|
||||
shouldSelect: false,
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results: results2 } = getProps( renderer );
|
||||
expect( results2 ).toBe( results );
|
||||
// expect 2 calls because internally, useSelect invokes callback twice
|
||||
// on mount.
|
||||
expect( mocks.selectors.getCollection ).toHaveBeenCalledTimes( 2 );
|
||||
|
||||
// rerender again but set shouldSelect to true again and we should see
|
||||
// new results
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
options: {
|
||||
namespace: 'test/store',
|
||||
resourceName: 'productsb',
|
||||
resourceValues: [ 10, 30 ],
|
||||
shouldSelect: true,
|
||||
},
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { results: results3 } = getProps( renderer );
|
||||
expect( results3 ).not.toEqual( results );
|
||||
expect( results3 ).toEqual( [
|
||||
'test/store',
|
||||
'productsb',
|
||||
{},
|
||||
[ 10, 30 ],
|
||||
] );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { usePrevious } from '../use-previous';
|
||||
|
||||
describe( 'usePrevious', () => {
|
||||
const TestComponent = ( { testValue, validation } ) => {
|
||||
const previousValue = usePrevious( testValue, validation );
|
||||
return <div testValue={ testValue } previousValue={ previousValue } />;
|
||||
};
|
||||
|
||||
let renderer;
|
||||
beforeEach( () => ( renderer = null ) );
|
||||
|
||||
it( 'should be undefined at first pass', () => {
|
||||
act( () => {
|
||||
renderer = TestRenderer.create( <TestComponent testValue={ 1 } /> );
|
||||
} );
|
||||
const testValue = renderer.root.findByType( 'div' ).props.testValue;
|
||||
const testPreviousValue = renderer.root.findByType( 'div' ).props
|
||||
.previousValue;
|
||||
|
||||
expect( testValue ).toBe( 1 );
|
||||
expect( testPreviousValue ).toBe( undefined );
|
||||
} );
|
||||
|
||||
it( 'test new and previous value', () => {
|
||||
let testValue;
|
||||
let testPreviousValue;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create( <TestComponent testValue={ 1 } /> );
|
||||
} );
|
||||
|
||||
act( () => {
|
||||
renderer.update( <TestComponent testValue={ 2 } /> );
|
||||
} );
|
||||
testValue = renderer.root.findByType( 'div' ).props.testValue;
|
||||
testPreviousValue = renderer.root.findByType( 'div' ).props
|
||||
.previousValue;
|
||||
expect( testValue ).toBe( 2 );
|
||||
expect( testPreviousValue ).toBe( 1 );
|
||||
|
||||
act( () => {
|
||||
renderer.update( <TestComponent testValue={ 3 } /> );
|
||||
} );
|
||||
testValue = renderer.root.findByType( 'div' ).props.testValue;
|
||||
testPreviousValue = renderer.root.findByType( 'div' ).props
|
||||
.previousValue;
|
||||
expect( testValue ).toBe( 3 );
|
||||
expect( testPreviousValue ).toBe( 2 );
|
||||
} );
|
||||
|
||||
it( 'should not update value if validation fails', () => {
|
||||
let testValue;
|
||||
let testPreviousValue;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
<TestComponent testValue={ 1 } validation={ Number.isFinite } />
|
||||
);
|
||||
} );
|
||||
|
||||
act( () => {
|
||||
renderer.update(
|
||||
<TestComponent testValue="abc" validation={ Number.isFinite } />
|
||||
);
|
||||
} );
|
||||
testValue = renderer.root.findByType( 'div' ).props.testValue;
|
||||
testPreviousValue = renderer.root.findByType( 'div' ).props
|
||||
.previousValue;
|
||||
expect( testValue ).toBe( 'abc' );
|
||||
expect( testPreviousValue ).toBe( 1 );
|
||||
|
||||
act( () => {
|
||||
renderer.update(
|
||||
<TestComponent testValue={ 3 } validation={ Number.isFinite } />
|
||||
);
|
||||
} );
|
||||
testValue = renderer.root.findByType( 'div' ).props.testValue;
|
||||
testPreviousValue = renderer.root.findByType( 'div' ).props
|
||||
.previousValue;
|
||||
expect( testValue ).toBe( 3 );
|
||||
expect( testPreviousValue ).toBe( 1 );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
import { QUERY_STATE_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
useQueryStateByContext,
|
||||
useQueryStateByKey,
|
||||
useSynchronizedQueryState,
|
||||
} from '../use-query-state';
|
||||
|
||||
jest.mock( '@woocommerce/block-data', () => ( {
|
||||
__esModule: true,
|
||||
QUERY_STATE_STORE_KEY: 'test/store',
|
||||
} ) );
|
||||
|
||||
describe( 'Testing Query State Hooks', () => {
|
||||
let registry, mocks;
|
||||
beforeAll( () => {
|
||||
registry = createRegistry();
|
||||
mocks = {};
|
||||
} );
|
||||
/**
|
||||
* Test helper to return a tuple containing the expected query value and the
|
||||
* expected query state action creator from the given rendered test instance.
|
||||
*
|
||||
* @param {Object} testRenderer An instance of the created test component.
|
||||
*
|
||||
* @return {Array} A tuple containing the expected query value as the first
|
||||
* element and the expected query action creator as the
|
||||
* second argument.
|
||||
*/
|
||||
const getProps = ( testRenderer ) => {
|
||||
const props = testRenderer.root.findByType( 'div' ).props;
|
||||
return [ props.queryState, props.setQueryState ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the given component wrapped in the registry provider for
|
||||
* instantiating using the TestRenderer using the current prepared registry
|
||||
* for the TestRenderer to instantiate with.
|
||||
*
|
||||
* @param {React.Component} Component The test component to wrap.
|
||||
* @param {Object} props Props to feed the wrapped component.
|
||||
*
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const getWrappedComponent = ( Component, props ) => (
|
||||
<RegistryProvider value={ registry }>
|
||||
<Component { ...props } />
|
||||
</RegistryProvider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns a TestComponent for the provided hook to test with, and the
|
||||
* expected PropKeys for obtaining the values to be fed to the hook as
|
||||
* arguments.
|
||||
*
|
||||
* @param {Function} hookTested The hook being tested to use in the
|
||||
* test comopnent.
|
||||
* @param {Array} propKeysForArgs An array of keys for the props that
|
||||
* will be used on the test component that
|
||||
* will have values fed to the tested
|
||||
* hook.
|
||||
*
|
||||
* @return {React.Component} A component ready for testing with!
|
||||
*/
|
||||
const getTestComponent = ( hookTested, propKeysForArgs ) => ( props ) => {
|
||||
const args = propKeysForArgs.map( ( key ) => props[ key ] );
|
||||
const [ queryValue, setQueryValue ] = hookTested( ...args );
|
||||
return (
|
||||
<div queryState={ queryValue } setQueryState={ setQueryValue } />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A helper for setting up the `mocks` object and the `registry` mock before
|
||||
* each test.
|
||||
*
|
||||
* @param {string} actionMockName This should be the name of the action
|
||||
* that the hook returns. This will be
|
||||
* mocked using `mocks.action` when
|
||||
* registered in the mock registry.
|
||||
* @param {string} selectorMockName This should be the mame of the selector
|
||||
* that the hook uses. This will be mocked
|
||||
* using `mocks.selector` when registered
|
||||
* in the mock registry.
|
||||
*/
|
||||
const setupMocks = ( actionMockName, selectorMockName ) => {
|
||||
mocks.action = jest.fn().mockReturnValue( { type: 'testAction' } );
|
||||
mocks.selector = jest.fn().mockReturnValue( { foo: 'bar' } );
|
||||
registry.registerStore( storeKey, {
|
||||
reducer: () => ( {} ),
|
||||
actions: {
|
||||
[ actionMockName ]: mocks.action,
|
||||
},
|
||||
selectors: {
|
||||
[ selectorMockName ]: mocks.selector,
|
||||
},
|
||||
} );
|
||||
};
|
||||
describe( 'useQueryStateByContext', () => {
|
||||
const TestComponent = getTestComponent( useQueryStateByContext, [
|
||||
'context',
|
||||
] );
|
||||
let renderer;
|
||||
beforeEach( () => {
|
||||
renderer = null;
|
||||
setupMocks( 'setValueForQueryContext', 'getValueForQueryContext' );
|
||||
} );
|
||||
afterEach( () => {
|
||||
act( () => {
|
||||
renderer.unmount();
|
||||
} );
|
||||
} );
|
||||
it(
|
||||
'calls useSelect with the provided context and returns expected' +
|
||||
' values',
|
||||
() => {
|
||||
const { action, selector } = mocks;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponent( TestComponent, {
|
||||
context: 'test-context',
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const [ queryState, setQueryState ] = getProps( renderer );
|
||||
// the {} is because all selectors are called internally in the
|
||||
// registry with the first argument being the state which is empty.
|
||||
expect( selector ).toHaveBeenLastCalledWith(
|
||||
{},
|
||||
'test-context',
|
||||
undefined
|
||||
);
|
||||
expect( queryState ).toEqual( { foo: 'bar' } );
|
||||
expect( action ).not.toHaveBeenCalled();
|
||||
|
||||
//execute dispatcher and make sure it's called.
|
||||
act( () => {
|
||||
setQueryState( { foo: 'bar' } );
|
||||
} );
|
||||
expect( action ).toHaveBeenCalledWith( 'test-context', {
|
||||
foo: 'bar',
|
||||
} );
|
||||
}
|
||||
);
|
||||
} );
|
||||
describe( 'useQueryStateByKey', () => {
|
||||
const TestComponent = getTestComponent( useQueryStateByKey, [
|
||||
'queryKey',
|
||||
undefined,
|
||||
'context',
|
||||
] );
|
||||
let renderer;
|
||||
beforeEach( () => {
|
||||
renderer = null;
|
||||
setupMocks( 'setQueryValue', 'getValueForQueryKey' );
|
||||
} );
|
||||
afterEach( () => {
|
||||
act( () => {
|
||||
renderer.unmount();
|
||||
} );
|
||||
} );
|
||||
it(
|
||||
'calls useSelect with the provided context and returns expected' +
|
||||
' values',
|
||||
() => {
|
||||
const { selector, action } = mocks;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponent( TestComponent, {
|
||||
context: 'test-context',
|
||||
queryKey: 'someValue',
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const [ queryState, setQueryState ] = getProps( renderer );
|
||||
// the {} is because all selectors are called internally in the
|
||||
// registry with the first argument being the state which is empty.
|
||||
expect( selector ).toHaveBeenLastCalledWith(
|
||||
{},
|
||||
'test-context',
|
||||
'someValue',
|
||||
undefined
|
||||
);
|
||||
expect( queryState ).toEqual( { foo: 'bar' } );
|
||||
expect( action ).not.toHaveBeenCalled();
|
||||
|
||||
//execute dispatcher and make sure it's called.
|
||||
act( () => {
|
||||
setQueryState( { foo: 'bar' } );
|
||||
} );
|
||||
expect( action ).toHaveBeenCalledWith(
|
||||
'test-context',
|
||||
'someValue',
|
||||
{ foo: 'bar' }
|
||||
);
|
||||
}
|
||||
);
|
||||
} );
|
||||
// @todo, these tests only add partial coverage because the state is not
|
||||
// actually updated by the action dispatch via our mocks.
|
||||
describe( 'useSynchronizedQueryState', () => {
|
||||
const TestComponent = getTestComponent( useSynchronizedQueryState, [
|
||||
'synchronizedQuery',
|
||||
'context',
|
||||
] );
|
||||
const initialQuery = { a: 'b' };
|
||||
let renderer;
|
||||
beforeEach( () => {
|
||||
setupMocks( 'setValueForQueryContext', 'getValueForQueryContext' );
|
||||
} );
|
||||
it( 'returns provided query state on initial render', () => {
|
||||
const { action, selector } = mocks;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponent( TestComponent, {
|
||||
context: 'test-context',
|
||||
synchronizedQuery: initialQuery,
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const [ queryState ] = getProps( renderer );
|
||||
expect( queryState ).toBe( initialQuery );
|
||||
expect( selector ).toHaveBeenLastCalledWith(
|
||||
{},
|
||||
'test-context',
|
||||
undefined
|
||||
);
|
||||
expect( action ).toHaveBeenCalledWith( 'test-context', {
|
||||
foo: 'bar',
|
||||
a: 'b',
|
||||
} );
|
||||
} );
|
||||
it( 'returns merged queryState on subsequent render', () => {
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponent( TestComponent, {
|
||||
context: 'test-context',
|
||||
synchronizedQuery: initialQuery,
|
||||
} )
|
||||
);
|
||||
} );
|
||||
// note our test doesn't interact with an actual reducer so the
|
||||
// store state is not updated. Here we're just verifying that
|
||||
// what is is returned by the state selector mock is returned.
|
||||
// However we DO expect this to be a new object.
|
||||
const [ queryState ] = getProps( renderer );
|
||||
expect( queryState ).not.toBe( initialQuery );
|
||||
expect( queryState ).toEqual( { foo: 'bar' } );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useShallowEqual } from '../use-shallow-equal';
|
||||
|
||||
describe( 'useShallowEqual', () => {
|
||||
const TestComponent = ( { testValue } ) => {
|
||||
const newValue = useShallowEqual( testValue );
|
||||
return <div newValue={ newValue } />;
|
||||
};
|
||||
let renderer;
|
||||
beforeEach( () => ( renderer = null ) );
|
||||
it.each`
|
||||
testValueA | aType | testValueB | bType | expectEqual
|
||||
${{ a: 'b', foo: 'bar' }} | ${'object'} | ${{ foo: 'bar', a: 'b' }} | ${'object'} | ${true}
|
||||
${{ a: 'b', foo: 'bar' }} | ${'object'} | ${{ foo: 'bar', a: 'c' }} | ${'object'} | ${false}
|
||||
${[ 'b', 'bar' ]} | ${'array'} | ${[ 'b', 'bar' ]} | ${'array'} | ${true}
|
||||
${[ 'b', 'bar' ]} | ${'array'} | ${[ 'bar', 'b' ]} | ${'array'} | ${false}
|
||||
${1} | ${'number'} | ${1} | ${'number'} | ${true}
|
||||
${1} | ${'number'} | ${'1'} | ${'string'} | ${false}
|
||||
${'1'} | ${'string'} | ${'1'} | ${'string'} | ${true}
|
||||
${1} | ${'number'} | ${2} | ${'number'} | ${false}
|
||||
${1} | ${'number'} | ${true} | ${'bool'} | ${false}
|
||||
${0} | ${'number'} | ${false} | ${'bool'} | ${false}
|
||||
${true} | ${'bool'} | ${true} | ${'bool'} | ${true}
|
||||
`(
|
||||
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal ($expectEqual)',
|
||||
( { testValueA, testValueB, expectEqual } ) => {
|
||||
let testPropValue;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
<TestComponent testValue={ testValueA } />
|
||||
);
|
||||
} );
|
||||
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
|
||||
expect( testPropValue ).toBe( testValueA );
|
||||
// do update
|
||||
act( () => {
|
||||
renderer.update( <TestComponent testValue={ testValueB } /> );
|
||||
} );
|
||||
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
|
||||
if ( expectEqual ) {
|
||||
expect( testPropValue ).toBe( testValueA );
|
||||
} else {
|
||||
expect( testPropValue ).toBe( testValueB );
|
||||
}
|
||||
}
|
||||
);
|
||||
} );
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer, { act } from 'react-test-renderer';
|
||||
import { createRegistry, RegistryProvider } from '@wordpress/data';
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreProducts } from '../use-store-products';
|
||||
|
||||
jest.mock( '@woocommerce/block-data', () => ( {
|
||||
__esModule: true,
|
||||
COLLECTIONS_STORE_KEY: 'test/store',
|
||||
} ) );
|
||||
|
||||
describe( 'useStoreProducts', () => {
|
||||
let registry, mocks, renderer;
|
||||
const getProps = ( testRenderer ) => {
|
||||
const {
|
||||
products,
|
||||
totalProducts,
|
||||
productsLoading,
|
||||
} = testRenderer.root.findByType( 'div' ).props;
|
||||
return {
|
||||
products,
|
||||
totalProducts,
|
||||
productsLoading,
|
||||
};
|
||||
};
|
||||
|
||||
const getWrappedComponents = ( Component, props ) => (
|
||||
<RegistryProvider value={ registry }>
|
||||
<Component { ...props } />
|
||||
</RegistryProvider>
|
||||
);
|
||||
|
||||
const getTestComponent = () => ( { query } ) => {
|
||||
const items = useStoreProducts( query );
|
||||
return <div { ...items } />;
|
||||
};
|
||||
|
||||
const setUpMocks = () => {
|
||||
mocks = {
|
||||
selectors: {
|
||||
getCollection: jest
|
||||
.fn()
|
||||
.mockImplementation( () => ( { foo: 'bar' } ) ),
|
||||
getCollectionHeader: jest.fn().mockReturnValue( 22 ),
|
||||
hasFinishedResolution: jest.fn().mockReturnValue( true ),
|
||||
},
|
||||
};
|
||||
registry.registerStore( storeKey, {
|
||||
reducer: () => ( {} ),
|
||||
selectors: mocks.selectors,
|
||||
} );
|
||||
};
|
||||
|
||||
beforeEach( () => {
|
||||
registry = createRegistry();
|
||||
mocks = {};
|
||||
renderer = null;
|
||||
setUpMocks();
|
||||
} );
|
||||
it(
|
||||
'should return expected behaviour for equivalent query on props ' +
|
||||
'across renders',
|
||||
() => {
|
||||
const TestComponent = getTestComponent();
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { bar: 'foo' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { products } = getProps( renderer );
|
||||
// rerender
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { bar: 'foo' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
// re-render should result in same products object because although
|
||||
// query-state is a different instance, it's still equivalent.
|
||||
const { products: newProducts } = getProps( renderer );
|
||||
expect( newProducts ).toBe( products );
|
||||
// now let's change the query passed through to verify new object
|
||||
// is created.
|
||||
// remember this won't actually change the results because the mock
|
||||
// selector is returning an equivalent object when it is called,
|
||||
// however it SHOULD be a new object instance.
|
||||
act( () => {
|
||||
renderer.update(
|
||||
getWrappedComponents( TestComponent, {
|
||||
query: { foo: 'bar' },
|
||||
} )
|
||||
);
|
||||
} );
|
||||
const { products: productsVerification } = getProps( renderer );
|
||||
expect( productsVerification ).not.toBe( products );
|
||||
expect( productsVerification ).toEqual( products );
|
||||
renderer.unmount();
|
||||
}
|
||||
);
|
||||
} );
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect, useMemo } from '@wordpress/element';
|
||||
import {
|
||||
useQueryStateByContext,
|
||||
useQueryStateByKey,
|
||||
useCollection,
|
||||
} from '@woocommerce/base-hooks';
|
||||
import { useQueryStateContext } from '@woocommerce/base-context/query-state-context';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useShallowEqual } from './use-shallow-equal';
|
||||
|
||||
const buildCollectionDataQuery = ( collectionDataQueryState ) => {
|
||||
const query = collectionDataQueryState;
|
||||
|
||||
if ( collectionDataQueryState.calculate_attribute_counts ) {
|
||||
query.calculate_attribute_counts = sortBy(
|
||||
collectionDataQueryState.calculate_attribute_counts.map(
|
||||
( { taxonomy, queryType } ) => {
|
||||
return {
|
||||
taxonomy,
|
||||
query_type: queryType,
|
||||
};
|
||||
}
|
||||
),
|
||||
[ 'taxonomy', 'query_type' ]
|
||||
);
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const useCollectionData = ( {
|
||||
queryAttribute,
|
||||
queryPrices,
|
||||
queryState,
|
||||
} ) => {
|
||||
let context = useQueryStateContext();
|
||||
context = `${ context }-collection-data`;
|
||||
|
||||
const [ collectionDataQueryState ] = useQueryStateByContext( context );
|
||||
const [
|
||||
calculateAttributesQueryState,
|
||||
setCalculateAttributesQueryState,
|
||||
] = useQueryStateByKey( 'calculate_attribute_counts', [], context );
|
||||
const [
|
||||
calculatePriceRangeQueryState,
|
||||
setCalculatePriceRangeQueryState,
|
||||
] = useQueryStateByKey( 'calculate_price_range', null, context );
|
||||
|
||||
const currentQueryAttribute = useShallowEqual( queryAttribute || {} );
|
||||
const currentQueryPrices = useShallowEqual( queryPrices );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
typeof currentQueryAttribute === 'object' &&
|
||||
Object.keys( currentQueryAttribute ).length
|
||||
) {
|
||||
const foundAttribute = calculateAttributesQueryState.find(
|
||||
( attribute ) => {
|
||||
return (
|
||||
attribute.taxonomy === currentQueryAttribute.taxonomy
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! foundAttribute ) {
|
||||
setCalculateAttributesQueryState( [
|
||||
...calculateAttributesQueryState,
|
||||
currentQueryAttribute,
|
||||
] );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentQueryAttribute,
|
||||
calculateAttributesQueryState,
|
||||
setCalculateAttributesQueryState,
|
||||
] );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
calculatePriceRangeQueryState !== currentQueryPrices &&
|
||||
currentQueryPrices !== undefined
|
||||
) {
|
||||
setCalculatePriceRangeQueryState( currentQueryPrices );
|
||||
}
|
||||
}, [
|
||||
currentQueryPrices,
|
||||
setCalculatePriceRangeQueryState,
|
||||
calculatePriceRangeQueryState,
|
||||
] );
|
||||
|
||||
// Defer the select query so all collection-data query vars can be gathered.
|
||||
const [ shouldSelect, setShouldSelect ] = useState( false );
|
||||
const [ debouncedShouldSelect ] = useDebounce( shouldSelect, 200 );
|
||||
|
||||
if ( ! shouldSelect ) {
|
||||
setShouldSelect( true );
|
||||
}
|
||||
|
||||
const collectionDataQueryVars = useMemo( () => {
|
||||
return buildCollectionDataQuery( collectionDataQueryState );
|
||||
}, [ collectionDataQueryState ] );
|
||||
|
||||
return useCollection( {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products/collection-data',
|
||||
query: {
|
||||
...queryState,
|
||||
page: undefined,
|
||||
per_page: undefined,
|
||||
orderby: undefined,
|
||||
order: undefined,
|
||||
...collectionDataQueryVars,
|
||||
},
|
||||
shouldSelect: debouncedShouldSelect,
|
||||
} );
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useShallowEqual } from './use-shallow-equal';
|
||||
|
||||
/**
|
||||
* This is a custom hook that is wired up to the `wc/store/collections` data
|
||||
* store. Given a header key and a collections option object, this will ensure a
|
||||
* component is kept up to date with the collection header value matching that
|
||||
* query in the store state.
|
||||
*
|
||||
* @param {string} headerKey Used to indicate which header value to
|
||||
* return for the given collection query.
|
||||
* Example: `'x-wp-total'`
|
||||
* @param {Object} options An object declaring the various
|
||||
* collection arguments.
|
||||
* @param {string} options.namespace The namespace for the collection.
|
||||
* Example: `'/wc/blocks'`
|
||||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {Array} options.resourceValues An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
* @param {Object} options.query An object of key value pairs for the
|
||||
* query to execute on the collection
|
||||
* (optional). Example:
|
||||
* `{ order: 'ASC', order_by: 'price' }`
|
||||
*
|
||||
* @return {Object} This hook will return an object with two properties:
|
||||
* - value Whatever value is attached to the specified
|
||||
* header.
|
||||
* - isLoading A boolean indicating whether the header is
|
||||
* loading (true) or not.
|
||||
*/
|
||||
export const useCollectionHeader = ( headerKey, options ) => {
|
||||
const {
|
||||
namespace,
|
||||
resourceName,
|
||||
resourceValues = [],
|
||||
query = {},
|
||||
} = options;
|
||||
if ( ! namespace || ! resourceName ) {
|
||||
throw new Error(
|
||||
'The options object must have valid values for the namespace and ' +
|
||||
'the resource name properties.'
|
||||
);
|
||||
}
|
||||
// ensure we feed the previous reference if it's equivalent
|
||||
const currentQuery = useShallowEqual( query );
|
||||
const currentResourceValues = useShallowEqual( resourceValues );
|
||||
const { value, isLoading = true } = useSelect(
|
||||
( select ) => {
|
||||
const store = select( storeKey );
|
||||
// filter out query if it is undefined.
|
||||
const args = [
|
||||
headerKey,
|
||||
namespace,
|
||||
resourceName,
|
||||
currentQuery,
|
||||
currentResourceValues,
|
||||
];
|
||||
return {
|
||||
value: store.getCollectionHeader( ...args ),
|
||||
isLoading: store.hasFinishedResolution(
|
||||
'getCollectionHeader',
|
||||
args
|
||||
),
|
||||
};
|
||||
},
|
||||
[
|
||||
headerKey,
|
||||
namespace,
|
||||
resourceName,
|
||||
currentResourceValues,
|
||||
currentQuery,
|
||||
]
|
||||
);
|
||||
return {
|
||||
value,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { COLLECTIONS_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useRef } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useShallowEqual } from './use-shallow-equal';
|
||||
|
||||
/**
|
||||
* This is a custom hook that is wired up to the `wc/store/collections` data
|
||||
* store. Given a collections option object, this will ensure a component is
|
||||
* kept up to date with the collection matching that query in the store state.
|
||||
*
|
||||
* @param {Object} options An object declaring the various
|
||||
* collection arguments.
|
||||
* @param {string} options.namespace The namespace for the collection.
|
||||
* Example: `'/wc/blocks'`
|
||||
* @param {string} options.resourceName The name of the resource for the
|
||||
* collection. Example:
|
||||
* `'products/attributes'`
|
||||
* @param {Array} options.resourceValues An array of values (in correct order)
|
||||
* that are substituted in the route
|
||||
* placeholders for the collection route.
|
||||
* Example: `[10, 20]`
|
||||
* @param {Object} options.query An object of key value pairs for the
|
||||
* query to execute on the collection
|
||||
* (optional). Example:
|
||||
* `{ order: 'ASC', order_by: 'price' }`
|
||||
* @param {boolean} options.shouldSelect If false, the previous results will be
|
||||
* returned and internal selects will not
|
||||
* fire.
|
||||
*
|
||||
* @return {Object} This hook will return an object with two properties:
|
||||
* - results An array of collection items returned.
|
||||
* - isLoading A boolean indicating whether the collection is
|
||||
* loading (true) or not.
|
||||
*/
|
||||
export const useCollection = ( options ) => {
|
||||
const {
|
||||
namespace,
|
||||
resourceName,
|
||||
resourceValues = [],
|
||||
query = {},
|
||||
shouldSelect = true,
|
||||
} = options;
|
||||
if ( ! namespace || ! resourceName ) {
|
||||
throw new Error(
|
||||
'The options object must have valid values for the namespace and ' +
|
||||
'the resource properties.'
|
||||
);
|
||||
}
|
||||
const currentResults = useRef( { results: [], isLoading: true } );
|
||||
// ensure we feed the previous reference if it's equivalent
|
||||
const currentQuery = useShallowEqual( query );
|
||||
const currentResourceValues = useShallowEqual( resourceValues );
|
||||
const results = useSelect(
|
||||
( select ) => {
|
||||
if ( ! shouldSelect ) {
|
||||
return null;
|
||||
}
|
||||
const store = select( storeKey );
|
||||
// filter out query if it is undefined.
|
||||
const args = [
|
||||
namespace,
|
||||
resourceName,
|
||||
currentQuery,
|
||||
currentResourceValues,
|
||||
];
|
||||
return {
|
||||
results: store.getCollection( ...args ),
|
||||
isLoading: ! store.hasFinishedResolution(
|
||||
'getCollection',
|
||||
args
|
||||
),
|
||||
};
|
||||
},
|
||||
[
|
||||
namespace,
|
||||
resourceName,
|
||||
currentResourceValues,
|
||||
currentQuery,
|
||||
shouldSelect,
|
||||
]
|
||||
);
|
||||
// if selector was not bailed, then update current results. Otherwise return
|
||||
// previous results
|
||||
if ( results !== null ) {
|
||||
currentResults.current = results;
|
||||
}
|
||||
return currentResults.current;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Use Previous based on https://usehooks.com/usePrevious/.
|
||||
* @param {mixed} value
|
||||
* @param {Function} [validation] Function that needs to validate for the value
|
||||
* to be updated.
|
||||
*/
|
||||
export const usePrevious = ( value, validation ) => {
|
||||
const ref = useRef();
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
ref.current !== value &&
|
||||
( ! validation || validation( value, ref.current ) )
|
||||
) {
|
||||
ref.current = value;
|
||||
}
|
||||
}, [ value, ref.current ] );
|
||||
|
||||
return ref.current;
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { QUERY_STATE_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useRef, useEffect, useCallback } from '@wordpress/element';
|
||||
import { useQueryStateContext } from '@woocommerce/base-context/query-state-context';
|
||||
import { assign } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useShallowEqual } from './use-shallow-equal';
|
||||
|
||||
/**
|
||||
* A custom hook that exposes the current query state and a setter for the query
|
||||
* state store for the given context.
|
||||
*
|
||||
* "Query State" is a wp.data store that keeps track of an arbitrary object of
|
||||
* query keys and their values.
|
||||
*
|
||||
* @param {string} [context] What context to retrieve the query state for. If not
|
||||
* provided, this hook will attempt to get the context
|
||||
* from the query state context provided by the
|
||||
* QueryStateContextProvider
|
||||
*
|
||||
* @return {Array} An array that has two elements. The first element is the
|
||||
* query state value for the given context. The second element
|
||||
* is a dispatcher function for setting the query state.
|
||||
*/
|
||||
export const useQueryStateByContext = ( context ) => {
|
||||
const queryStateContext = useQueryStateContext();
|
||||
context = context || queryStateContext;
|
||||
const queryState = useSelect(
|
||||
( select ) => {
|
||||
const store = select( storeKey );
|
||||
return store.getValueForQueryContext( context, undefined );
|
||||
},
|
||||
[ context ]
|
||||
);
|
||||
const { setValueForQueryContext } = useDispatch( storeKey );
|
||||
const setQueryState = useCallback(
|
||||
( value ) => {
|
||||
setValueForQueryContext( context, value );
|
||||
},
|
||||
[ context ]
|
||||
);
|
||||
|
||||
return [ queryState, setQueryState ];
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom hook that exposes the current query state value and a setter for the
|
||||
* given context and query key.
|
||||
*
|
||||
* "Query State" is a wp.data store that keeps track of an arbitrary object of
|
||||
* query keys and their values.
|
||||
*
|
||||
* @param {*} queryKey The specific query key to retrieve the value for.
|
||||
* @param {*} defaultValue Default value if query does not exist.
|
||||
* @param {string} [context] What context to retrieve the query state for. If
|
||||
* not provided will attempt to use what is provided
|
||||
* by query state context.
|
||||
*
|
||||
* @return {*} Whatever value is set at the query state index using the
|
||||
* provided context and query key.
|
||||
*/
|
||||
export const useQueryStateByKey = ( queryKey, defaultValue, context ) => {
|
||||
const queryStateContext = useQueryStateContext();
|
||||
context = context || queryStateContext;
|
||||
const queryValue = useSelect(
|
||||
( select ) => {
|
||||
const store = select( storeKey );
|
||||
return store.getValueForQueryKey( context, queryKey, defaultValue );
|
||||
},
|
||||
[ context, queryKey ]
|
||||
);
|
||||
|
||||
const { setQueryValue } = useDispatch( storeKey );
|
||||
const setQueryValueByKey = useCallback(
|
||||
( value ) => {
|
||||
setQueryValue( context, queryKey, value );
|
||||
},
|
||||
[ context, queryKey ]
|
||||
);
|
||||
|
||||
return [ queryValue, setQueryValueByKey ];
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom hook that works similarly to useQueryStateByContext. However, this
|
||||
* hook allows for synchronizing with a provided queryState object.
|
||||
*
|
||||
* This hook does the following things with the provided `synchronizedQuery`
|
||||
* object:
|
||||
*
|
||||
* - whenever synchronizedQuery varies between renders, the queryState will be
|
||||
* updated to a merged object of the internal queryState and the provided
|
||||
* object. Note, any values from the same properties between objects will
|
||||
* be set from synchronizedQuery.
|
||||
* - if there are no changes between renders, then the existing internal
|
||||
* queryState is always returned.
|
||||
* - on initial render, the synchronizedQuery value is returned.
|
||||
*
|
||||
* Typically, this hook would be used in a scenario where there may be external
|
||||
* triggers for updating the query state (i.e. initial population of query
|
||||
* state by hydration or component attributes, or routing url changes that
|
||||
* affect query state).
|
||||
*
|
||||
* @param {Object} synchronizedQuery A provided query state object to
|
||||
* synchronize internal query state with.
|
||||
* @param {string} [context] What context to retrieve the query state
|
||||
* for. If not provided, will be pulled from
|
||||
* the QueryStateContextProvider in the tree.
|
||||
*/
|
||||
export const useSynchronizedQueryState = ( synchronizedQuery, context ) => {
|
||||
const queryStateContext = useQueryStateContext();
|
||||
context = context || queryStateContext;
|
||||
const [ queryState, setQueryState ] = useQueryStateByContext( context );
|
||||
const currentSynchronizedQuery = useShallowEqual( synchronizedQuery );
|
||||
// used to ensure we allow initial synchronization to occur before
|
||||
// returning non-synced state.
|
||||
const isInitialized = useRef( false );
|
||||
// update queryState anytime incoming synchronizedQuery changes
|
||||
useEffect( () => {
|
||||
setQueryState( assign( {}, queryState, currentSynchronizedQuery ) );
|
||||
isInitialized.current = true;
|
||||
}, [ currentSynchronizedQuery ] );
|
||||
return isInitialized.current
|
||||
? [ queryState, setQueryState ]
|
||||
: [ synchronizedQuery, setQueryState ];
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useRef } from '@wordpress/element';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* A custom hook that compares the provided value across renders and returns the
|
||||
* previous instance if shallow equality with previous instance exists.
|
||||
*
|
||||
* This is particularly useful when non-primitive types are used as
|
||||
* dependencies for react hooks.
|
||||
*
|
||||
* @param {mixed} value Value to keep the same if satisfies shallow equality.
|
||||
*
|
||||
* @return {mixed} The previous cached instance of the value if the current has
|
||||
* shallow equality with it.
|
||||
*/
|
||||
export const useShallowEqual = ( value ) => {
|
||||
const ref = useRef();
|
||||
if ( ! isShallowEqual( value, ref.current ) ) {
|
||||
ref.current = value;
|
||||
}
|
||||
return ref.current;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCollection } from './use-collection';
|
||||
import { useCollectionHeader } from './use-collection-header';
|
||||
|
||||
/**
|
||||
* This is a custom hook that is wired up to the `wc/store/collections` data
|
||||
* store for the `'wc/store/products'` route. Given a query object, this
|
||||
* will ensure a component is kept up to date with the products matching that
|
||||
* query in the store state.
|
||||
*
|
||||
* @param {Object} query An object containing any query arguments to be
|
||||
* included with the collection request for the
|
||||
* products. Does not have to be included.
|
||||
*
|
||||
* @return {Object} This hook will return an object with three properties:
|
||||
* - products An array of product objects.
|
||||
* - totalProducts The total number of products that match
|
||||
* the given query parameters.
|
||||
* - productsLoading A boolean indicating whether the products
|
||||
* are still loading or not.
|
||||
*/
|
||||
export const useStoreProducts = ( query ) => {
|
||||
// @todo see @https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1097
|
||||
// where the namespace is going to be changed. Not doing in this pull.
|
||||
const collectionOptions = {
|
||||
namespace: '/wc/store',
|
||||
resourceName: 'products',
|
||||
};
|
||||
const { results: products, isLoading: productsLoading } = useCollection( {
|
||||
...collectionOptions,
|
||||
query,
|
||||
} );
|
||||
const { value: totalProducts } = useCollectionHeader( 'x-wp-total', {
|
||||
...collectionOptions,
|
||||
query,
|
||||
} );
|
||||
return {
|
||||
products,
|
||||
totalProducts,
|
||||
productsLoading,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user