Skip to content

Conversation

@epeicher
Copy link
Contributor

@epeicher epeicher commented Nov 25, 2025

Related issues

Before

CleanShot.2025-11-26.at.20.17.26-trimmed.mp4

After

CleanShot.2025-11-26.at.20.18.33.mp4

Proposed Changes

  • Migrated from custom useFetchWpComSites hook to RTK Query with useGetWpComSitesQuery. The new query uses the userId to invalidate the cache
  • Improved state management with automatic caching, background refetching, and request deduplication
  • Reorganized sync utilities:
    • Moved sync-related utilities from src/hooks/use-fetch-wpcom-sites/ to src/modules/sync/lib/
    • Consolidated types into src/modules/sync/types.ts
    • Created shared sync-support.ts module for getSyncSupport and helper functions
  • Updated all imports across 20+ files to reflect new structure
  • Removed deprecated hook directory
  • Enables by default the Streamline Onboarding feature flag

Testing Instructions

  • Apply this branch and run npm start
  • Use or create a site that does not have any connected sites
  • Click on Publish site button and observe that the loading experience is fine (there are no multiple loading indicators)
  • On the Sync tab, click on Publish site and check that the loading is minimal as the sites are cached
  • Test also the Pull site button
  • Delete or create a new site on WordPress.com
  • Click on the Publish site button, check that the deleted or created site is displayed (there could be a delay)
  • Log out and log in with a user with no sites
  • Click on the Publish site button
  • Check that the Find a perfect plan modal is displayed
  • Check different use cases and confirm that the sites are correctly loaded

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@epeicher epeicher marked this pull request as ready for review November 26, 2025 19:53
@github-actions
Copy link
Contributor

github-actions bot commented Nov 26, 2025

📊 Performance Test Results

Comparing 6ba4ba7 vs trunk

site-editor

Metric trunk 6ba4ba7 Diff Change
load 12705.00 ms 13875.00 ms +1170.00 ms 🔴 9.2%

site-startup

Metric trunk 6ba4ba7 Diff Change
siteCreation 19489.00 ms 22874.00 ms +3385.00 ms 🔴 17.4%
siteStartup 8005.00 ms 10068.00 ms +2063.00 ms 🔴 25.8%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change

Copy link
Member

@sejas sejas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Thanks for moving the wpcom sites request to RTK. Now it doesn't block the user every time we click on the buttons.
I just noticed that we display a loading state in the push site button every time we change tabs.

loading-push-site-button.mp4

dispatch( connectedSitesActions.openModal( pendingModalMode ) );
setPendingModalMode( null );
if ( isModalOpen && isAuthenticated && ! isUninitializedSyncSites ) {
refetchWpComSites().catch( ( error ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be possible to refetch wpcom sites inside openModal action or call it at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, I have done the changes on d7113b1, I think that is better.

I have also fixed the loading indicator in the button as part of 9a6f74f


function pruneCache(): void {
for ( const [ fn, cachedResults ] of cache ) {
for ( const [ _, cachedResults ] of cache ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lint fix, unrelated to this PR

Copy link
Member

@sejas sejas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@epeicher, thanks for making the adjustments. The PR looks good. I only found that Studio throws a warning when clicking Publish site from a different tab, like overview.

Failed to refetch sites: Error: Cannot refetch a query that has not been started yet.

Image

Comment on lines +15 to +18
// Schema for WordPress.com sites endpoint
const sitesEndpointSiteSchema = z.object( {
ID: z.number(),
is_wpcom_atomic: z.boolean(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have parts of this type duplicated in sync-support.ts. Can we unify them?

// Schema type for WordPress.com sites endpoint
export type SitesEndpointSite = {
ID: number;
is_wpcom_atomic: boolean;
name: string;
URL: string;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, this type can be inferred from the schema, I will update this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of 427d3ab, duplication removed, great catch!

Comment on lines 156 to 163
try {
const result = await refetchWpComSites();
return result.data ?? [];
} catch ( error ) {
// Query might not be ready to refetch yet (e.g., was skipped due to offline)
console.warn( 'Failed to refetch sites:', error );
return [];
}
Copy link
Member

@sejas sejas Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this pattern of catching the refetchWpComSite error repeated a few times. I wonder if makes sense to move the try catch inside the query and always return an empty array if it fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right, the refetchWpComSite is not always ready, so it can throw errors, I will encapsulate this on a function 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been simplified as part of 63cc6b8, as the effect is not required anymore after refetching the sites directly. There are not any errors now, but I have added the void operator to refetch the sites on the background (i.e. do not use await every time) and ignore any potential errors during the refetch, please let me know if you think it would be better to handle and maybe log them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I have consolidated both buttons into one with a condition that redirects to the Sync tab only when required in the commit 7e1bbdb

Copy link
Contributor

@katinthehatsite katinthehatsite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, nice work 👍 This is a huge PR and lots of work so well done!

I noticed one thing that when the sync modal is already open, I see the loading state on the Publish site button in the background:

Image

I also see it for the case when I click on the Publish site in the top right corner and then close the modal, the loading state persists in that case instead of going away.

setSelectedTab( 'sync' );
}
if ( isAuthenticated && ! isUninitializedSyncSites ) {
// Refetch sites on the background but ignore errors
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Refetch sites on the background but ignore errors
// Refetch sites in the background but ignore errors

The query is executed in the background so it is not required to indicate that in the button
@epeicher
Copy link
Contributor Author

epeicher commented Dec 1, 2025

Thanks for the review @katinthehatsite!

I noticed one thing that when the sync modal is already open, I see the loading state on the Publish site button in the background

Yes, that was expected as I included isBusy for the button while the sites are being fetched in the background, but I understand that seems confusing so I have removed it as part of b134968

Copy link
Contributor

@fredrikekelund fredrikekelund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great refactoring, I love to see it 👍

I left a couple of comments, mostly with a focus on how we can make the code more declarative and rely on RTK Query internals more, or move logic from components inside mutations.

I haven't tested extensively yet, but happy to do so in a second review later.

Comment on lines 36 to 30
const handlePublishClick = useCallback( () => {
if ( redirectToSync ) {
setSelectedTab( 'sync' );
}
if ( isAuthenticated && ! isUninitializedSyncSites ) {
// Refetch sites in the background but ignore errors
void refetchWpComSites();
}
dispatch( connectedSitesActions.openModal( 'push' ) );
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to call refetchWpComSites here? If the goal is to refetch whenever the sync modal opens, would it not make more sense to call this from within the sync modal?

We could combine it with the refetchOnMountOrArgChange option to refetch the hook data when mounting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say that's a matter of preference, I updated that motivated by this comment but I will investigate the result of using it when opening the sync modal and combining it with refetchOnMountOrArgChange

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After doing the changes, it's clear that it was better to do it in the component, thanks for the suggestion! I have done it as part of 5079edf

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving more of this logic to src/stores/sync/connected-sites.ts by updating the connectSite mutation like so:

connectSite: builder.mutation<
	SyncSite[],
	{ remoteSiteId: number; localSiteId: string; userId?: number }
>( {
	queryFn: async ( { remoteSiteId, localSiteId, userId }, api ) => {
		const connectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
		const { data: remoteSites = [] } = await api.dispatch(
			wpcomSitesApi.endpoints.getWpComSites.initiate( {
				connectedSiteIds: connectedSites.map( ( site ) => site.id ),
				userId,
			} )
		);
		const siteToConnect = remoteSites.find( ( site ) => site.id === remoteSiteId );

		if ( ! siteToConnect ) {
			return {
				error: { status: 'CUSTOM_ERROR', error: 'Site not found in WordPress.com sites' },
			};
		}

		await getIpcApi().connectWpcomSites( [
			{
				sites: [ siteToConnect ],
				localSiteId,
			},
		] );
	
		const actualConnectedSites = await getIpcApi().getConnectedWpcomSites( localSiteId );
	
		return { data: actualConnectedSites };
	},

This would allow us to shorten this hook significantly:

export function useListenDeepLinkConnection() {
	const [ connectSite ] = useConnectSiteMutation();
	const { selectedSite, setSelectedSiteId } = useSiteDetails();
	const { setSelectedTab, selectedTab } = useContentTabs();
	const { user } = useAuth();

	useIpcListener( 'sync-connect-site', async ( _event, { remoteSiteId, studioSiteId } ) => {
		if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
			// Select studio site that started the sync
			setSelectedSiteId( studioSiteId );
		}
		await connectSite( { remoteSiteId, localSiteId: studioSiteId, userId: user?.id } );
		if ( selectedTab !== 'sync' ) {
			// Switch to sync tab
			setSelectedTab( 'sync' );
		}
	} );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, I will work on that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been working on this, but I have found that this will trigger an additional call to the /me/sites endpoint when the connectSite mutation is used from the add-site and from the ContentTabSync handleConnect, where they already have the site, so I am not sure we are improving here.

What do you think about tackling this specific refactor as a follow-up and progressing with the current changes?

Comment on lines 132 to 135
const { data: syncSites = [], refetch: refetchSites } = useGetWpComSitesQuery( {
connectedSiteIds,
userId: user?.id,
} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { data: syncSites = [], refetch: refetchSites } = useGetWpComSitesQuery( {
connectedSiteIds,
userId: user?.id,
} );
const { data: syncSites = [] } = useGetWpComSitesQuery(
{ connectedSiteIds, userId: user?.id },
{ refetchOnMountOrArgChange: true }
);

I believe we could remove the refetchSites call by passing the refetchOnMountOrArgChange flag here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will have a look at it, I had that initially, but it was refetching the sites more than expected. I will check it again after latest updates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done the changes as part of 61e8a4b, they do the same refetch on mount and we avoid the useEffect 👍

Comment on lines 155 to 158
if ( isAuthenticated && ! isUninitializedSyncSites ) {
// Refetch sites on the background but ignore errors
void refetchWpComSites();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of explicitly refetching in the handleOpenModal handler, I suggest adding another useGetWpComSitesQuery call in SyncSitesModalSelector. See my comment in that file for more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a better approach, I have implemented it as part of 5079edf

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest making it so that the useGetWpComSitesQuery hook is declaratively refetched by removing the syncSites prop from this component, and adding another call to the useGetWpComSitesQuery hook like so:

	const { user } = useAuth();
	const { data: connectedSites = [] } = useGetConnectedSitesForLocalSiteQuery( {
		localSiteId: selectedSite.id,
		userId: user?.id,
	} );
	const connectedSiteIds = connectedSites.map( ( { id } ) => id );
	const { data: syncSites = [] } = useGetWpComSitesQuery(
		{ connectedSiteIds, userId: user?.id },
		{ refetchOnMountOrArgChange: true }
	);

The refetchOnMountOrArgChange option should accomplish the same thing as the refetch call in src/modules/sync/index.tsx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 😄 , I have done it as part of 5079edf

Comment on lines 151 to 164
// First transformation using all connected sites (for reconciliation)
const syncSitesForReconciliation = transformSitesResponse(
parsedResponse.sites,
allConnectedSites.map( ( { id } ) => id )
);

// whenever array of syncSites changes, we need to update connectedSites to keep them updated with wordpress.com
const { updatedConnectedSites } = reconcileConnectedSites(
allConnectedSites,
syncSitesForReconciliation
);
await getIpcApi().updateConnectedWpcomSites( updatedConnectedSites );

// Second transformation using connectedSiteIds parameter (for syncSupport calculation for selected site)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// First transformation using all connected sites (for reconciliation)
const syncSitesForReconciliation = transformSitesResponse(
parsedResponse.sites,
allConnectedSites.map( ( { id } ) => id )
);
// whenever array of syncSites changes, we need to update connectedSites to keep them updated with wordpress.com
const { updatedConnectedSites } = reconcileConnectedSites(
allConnectedSites,
syncSitesForReconciliation
);
await getIpcApi().updateConnectedWpcomSites( updatedConnectedSites );
// Second transformation using connectedSiteIds parameter (for syncSupport calculation for selected site)
const syncSitesForReconciliation = transformSitesResponse(
parsedResponse.sites,
allConnectedSites.map( ( { id } ) => id )
);
const { updatedConnectedSites } = reconcileConnectedSites(
allConnectedSites,
syncSitesForReconciliation
);
await getIpcApi().updateConnectedWpcomSites( updatedConnectedSites );

Nit, but the variable names are clear enough here that I would argue we could skip the line comments.

Copy link
Contributor Author

@epeicher epeicher Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of 99a6b55

};
}

function transformSitesResponse( sites: unknown[], connectedSiteIds: number[] ): SyncSite[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this function is the same as before, but I think the connectedSiteIds deserves a short explainer in a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm… ok

Copy link
Contributor Author

@epeicher epeicher Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done as part of 99a6b55

@epeicher
Copy link
Contributor Author

epeicher commented Dec 2, 2025

Thanks @fredrikekelund for the review and the suggestions! I have implemented most of them and I still want to look at this one and this one. I will let you know when they are addressed and we can do another review 🙏

@epeicher
Copy link
Contributor Author

epeicher commented Dec 3, 2025

Hi @fredrikekelund, I have implemented most of your suggestions, and replied to one of the suggestions here. I have tested it again, but this will benefit from another testing and some review after latest changes.

Could you please give another round of review and let me know if you find any issues? 🙏

@fredrikekelund
Copy link
Contributor

Taking a look now 👀

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants