diff --git a/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook b/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook
new file mode 100644
index 0000000000000..14f97d84416bf
--- /dev/null
+++ b/projects/js-packages/publicize-components/changelog/add-use-post-pre-publish-value-hook
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Added usePostPrePublishValue hook
diff --git a/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts
new file mode 100644
index 0000000000000..60d80b8b9d97f
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/index.ts
@@ -0,0 +1,39 @@
+import { useSelect } from '@wordpress/data';
+import { store as editorStore } from '@wordpress/editor';
+import { useEffect, useRef, useState } from '@wordpress/element';
+
+/**
+ * Preserve a value from the post pre-publish state.
+ *
+ * The value will stop updating after the post is published.
+ *
+ * @template V
+ * @param {V} value - The value to preserve.
+ *
+ * @return {V} The preserved value.
+ */
+export function usePostPrePublishValue< V >( value: V ) {
+ const isPublishing = useSelect(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `@wordpress/editor` is a nightmare to work with TypeScript
+ select => ( select( editorStore ) as any ).isPublishingPost(),
+ []
+ );
+
+ const [ currentValue, setCurrentValue ] = useState( value );
+
+ const valueFrozen = useRef( false );
+
+ useEffect( () => {
+ // Freeze the value after publishing starts.
+ if ( isPublishing ) {
+ valueFrozen.current = true;
+ }
+
+ // Since the value is not frozen yet, update the current value.
+ if ( ! valueFrozen.current ) {
+ setCurrentValue( value );
+ }
+ }, [ isPublishing, value ] );
+
+ return currentValue;
+}
diff --git a/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js
new file mode 100644
index 0000000000000..70b59a9ebdc08
--- /dev/null
+++ b/projects/js-packages/publicize-components/src/hooks/use-post-pre-publish-value/test/index.test.js
@@ -0,0 +1,79 @@
+import { act, renderHook } from '@testing-library/react';
+import { RegistryProvider } from '@wordpress/data';
+import { store as editorStore } from '@wordpress/editor';
+import { usePostPrePublishValue } from '../';
+import {
+ connections as connectionsList,
+ createRegistryWithStores,
+} from '../../../utils/test-utils';
+
+const connections = connectionsList.map( connection => ( { ...connection, enabled: true } ) );
+
+const post = {
+ jetpack_publicize_connections: [ connections[ 0 ] ],
+};
+
+describe( 'usePostPrePublishValue', () => {
+ it( 'should return the value by default', async () => {
+ const registry = createRegistryWithStores( post );
+
+ const { result } = renderHook( () => usePostPrePublishValue( 'test-value' ), {
+ wrapper: ( { children } ) => (
+ { children }
+ ),
+ } );
+
+ expect( result.current ).toBe( 'test-value' );
+ } );
+
+ it( 'should return the updated value when the post is not being published', async () => {
+ const registry = createRegistryWithStores( post );
+
+ const { rerender, result } = renderHook(
+ ( initialValue = 'first-value' ) => usePostPrePublishValue( initialValue ),
+ {
+ wrapper: ( { children } ) => (
+ { children }
+ ),
+ }
+ );
+
+ rerender( 'second-value' );
+
+ await act( async () => {
+ await registry.dispatch( editorStore ).editPost( {
+ status: 'draft',
+ content: 'Some test content',
+ } );
+ } );
+
+ expect( result.current ).toBe( 'second-value' );
+ } );
+
+ it( 'should preserve the pre-publish value', async () => {
+ const registry = createRegistryWithStores( post );
+
+ const { rerender, result } = renderHook(
+ ( initialValue = 'first-value' ) => usePostPrePublishValue( initialValue ),
+ {
+ wrapper: ( { children } ) => (
+ { children }
+ ),
+ }
+ );
+
+ rerender( 'second-value' );
+
+ await act( async () => {
+ registry.dispatch( editorStore ).editPost( {
+ status: 'publish',
+ content: 'Some test content',
+ } );
+ registry.dispatch( editorStore ).savePost();
+ } );
+
+ rerender( 'third-value' );
+
+ expect( result.current ).toBe( 'second-value' );
+ } );
+} );
diff --git a/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js b/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js
index 2593baa4d1c1d..c74bf768b4b3d 100644
--- a/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js
+++ b/projects/js-packages/publicize-components/src/hooks/use-sync-post-data-to-store/test/index.test.js
@@ -7,7 +7,7 @@ import { store as socialStore } from '../../../social-store';
import {
connections as connectionsList,
createRegistryWithStores,
- testPost,
+ postPublishFetchHandler,
} from '../../../utils/test-utils';
const connections = connectionsList.map( connection => ( { ...connection, enabled: true } ) );
@@ -16,9 +16,6 @@ const post = {
jetpack_publicize_connections: [ connections[ 0 ] ],
};
-const getMethod = options =>
- options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET';
-
describe( 'useSyncPostDataToStore', () => {
it( 'should do nothing by default', async () => {
const registry = createRegistryWithStores( post );
@@ -67,37 +64,7 @@ describe( 'useSyncPostDataToStore', () => {
await registry.resolveSelect( socialStore ).getConnections();
// Mock apiFetch response.
- apiFetch.setFetchHandler( async options => {
- const method = getMethod( options );
- const { path, data, parse = true } = options;
-
- const wrapReturn = parse
- ? v => v
- : v =>
- // Ideally we'd do `new Response( JSON.stringify( v ) )` here, but jsdom deletes that. Sigh.
- // See https://github.com/jsdom/jsdom/issues/1724
- ( {
- async json() {
- return v;
- },
- } );
-
- if ( method === 'PUT' && path.startsWith( `/wp/v2/posts/${ testPost.id }` ) ) {
- return wrapReturn( { ...post, ...data } );
- } else if (
- // This URL is requested by the actions dispatched in this test.
- // They are safe to ignore and are only listed here to avoid triggeringan error.
- method === 'GET' &&
- path.startsWith( '/wp/v2/types/post' )
- ) {
- return wrapReturn( {} );
- }
-
- throw {
- code: 'unknown_path',
- message: `Unknown path: ${ method } ${ path }`,
- };
- } );
+ apiFetch.setFetchHandler( postPublishFetchHandler( post ) );
const prevConnections = registry.select( socialStore ).getConnections();
diff --git a/projects/js-packages/publicize-components/src/utils/test-utils.js b/projects/js-packages/publicize-components/src/utils/test-utils.js
index 857e4ecc1585d..fa2f326be7bc6 100644
--- a/projects/js-packages/publicize-components/src/utils/test-utils.js
+++ b/projects/js-packages/publicize-components/src/utils/test-utils.js
@@ -118,3 +118,54 @@ export function createActiveConnections( count ) {
},
];
}
+
+const getMethod = options =>
+ options.headers?.[ 'X-HTTP-Method-Override' ] || options.method || 'GET';
+
+/**
+ * Get the mocked fetch handler for post publish fetch requests.
+ *
+ * @param {Record} postData - Data to be used in the fetch request.
+ *
+ * @return {(options: import('@wordpress/api-fetch/build-types/types').APIFetchOptions) => Promise} Promise resolving to the fetch response
+ */
+export function postPublishFetchHandler( postData ) {
+ /**
+ * The mocked fetch handler for post publish fetch requests.
+ *
+ * @param {import('@wordpress/api-fetch/build-types/types').APIFetchOptions} options - Fetch options.
+ *
+ * @return {Promise} Promise resolving to the fetch response
+ */
+ return async function ( options ) {
+ const method = getMethod( options );
+ const { path, data, parse = true } = options;
+
+ const wrapReturn = parse
+ ? v => v
+ : v =>
+ // Ideally we'd do `new Response( JSON.stringify( v ) )` here, but jsdom deletes that. Sigh.
+ // See https://github.com/jsdom/jsdom/issues/1724
+ ( {
+ async json() {
+ return v;
+ },
+ } );
+
+ if ( method === 'PUT' && path.startsWith( `/wp/v2/posts/${ testPost.id }` ) ) {
+ return wrapReturn( { ...postData, ...data } );
+ } else if (
+ // This URL is requested by the actions dispatched in this test.
+ // They are safe to ignore and are only listed here to avoid triggeringan error.
+ method === 'GET' &&
+ path.startsWith( '/wp/v2/types/post' )
+ ) {
+ return wrapReturn( {} );
+ }
+
+ throw {
+ code: 'unknown_path',
+ message: `Unknown path: ${ method } ${ path }`,
+ };
+ };
+}