Skip to content

Commit 10cd73d

Browse files
committed
Refactor and optimize the hook
1 parent b9d963c commit 10cd73d

File tree

2 files changed

+97
-129
lines changed

2 files changed

+97
-129
lines changed
Lines changed: 76 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
1-
/**
2-
* External dependencies
3-
*/
4-
import fastDeepEqual from 'fast-deep-equal/es6';
5-
61
/**
72
* WordPress dependencies
83
*/
9-
import { useDebounce, usePrevious } from '@wordpress/compose';
10-
import {
11-
useCallback,
12-
useEffect,
13-
useLayoutEffect,
14-
useRef,
15-
useState,
16-
} from '@wordpress/element';
4+
import { debounce } from '@wordpress/compose';
5+
import { useEffect, useState, useRef } from '@wordpress/element';
176
import apiFetch from '@wordpress/api-fetch';
187
import { addQueryArgs } from '@wordpress/url';
198
import { __experimentalSanitizeBlockAttributes } from '@wordpress/blocks';
@@ -50,111 +39,84 @@ export function removeBlockSupportAttributes( attributes ) {
5039
}
5140

5241
export function useServerSideRender( args ) {
53-
const isMountedRef = useRef( false );
54-
const fetchRequestRef = useRef();
55-
const latestArgssRef = useRef( args );
56-
const prevArgs = usePrevious( args );
57-
const [ response, setResponse ] = useState( null );
58-
const [ isLoading, setIsLoading ] = useState( false );
59-
60-
useLayoutEffect( () => {
61-
latestArgssRef.current = args;
62-
}, [ args ] );
63-
64-
const fetchData = useCallback( () => {
65-
if ( ! isMountedRef.current ) {
66-
return;
67-
}
68-
69-
const {
70-
attributes,
71-
block,
72-
skipBlockSupportAttributes = false,
73-
httpMethod = 'GET',
74-
urlQueryArgs,
75-
} = latestArgssRef.current;
76-
77-
setIsLoading( true );
78-
79-
let sanitizedAttributes =
80-
attributes &&
81-
__experimentalSanitizeBlockAttributes( block, attributes );
82-
83-
if ( skipBlockSupportAttributes ) {
84-
sanitizedAttributes =
85-
removeBlockSupportAttributes( sanitizedAttributes );
86-
}
87-
88-
// If httpMethod is 'POST', send the attributes in the request body instead of the URL.
89-
// This allows sending a larger attributes object than in a GET request, where the attributes are in the URL.
90-
const isPostRequest = 'POST' === httpMethod;
91-
const urlAttributes = isPostRequest
92-
? null
93-
: sanitizedAttributes ?? null;
94-
const path = rendererPath( block, urlAttributes, urlQueryArgs );
95-
const data = isPostRequest
96-
? { attributes: sanitizedAttributes ?? null }
97-
: null;
98-
99-
// Store the latest fetch request so that when we process it, we can
100-
// check if it is the current request, to avoid race conditions on slow networks.
101-
const fetchRequest = ( fetchRequestRef.current = apiFetch( {
102-
path,
103-
data,
104-
method: isPostRequest ? 'POST' : 'GET',
105-
} )
106-
.then( ( fetchResponse ) => {
107-
if (
108-
isMountedRef.current &&
109-
fetchRequest === fetchRequestRef.current &&
110-
fetchResponse
111-
) {
112-
setResponse( fetchResponse.rendered );
113-
}
114-
} )
115-
.catch( ( error ) => {
116-
if (
117-
isMountedRef.current &&
118-
fetchRequest === fetchRequestRef.current
119-
) {
120-
setResponse( {
121-
error: true,
122-
errorMsg: error.message,
123-
} );
124-
}
125-
} )
126-
.finally( () => {
127-
if (
128-
isMountedRef.current &&
129-
fetchRequest === fetchRequestRef.current
130-
) {
131-
setIsLoading( false );
132-
}
133-
} ) );
134-
135-
return fetchRequest;
136-
}, [] );
42+
const [ response, setResponse ] = useState( { status: 'idle' } );
43+
const shouldDebounceRef = useRef( false );
13744

138-
const debouncedFetchData = useDebounce( fetchData, 500 );
45+
const {
46+
attributes,
47+
block,
48+
skipBlockSupportAttributes = false,
49+
httpMethod = 'GET',
50+
urlQueryArgs,
51+
} = args;
52+
53+
let sanitizedAttributes =
54+
attributes &&
55+
__experimentalSanitizeBlockAttributes( block, attributes );
56+
57+
if ( skipBlockSupportAttributes ) {
58+
sanitizedAttributes =
59+
removeBlockSupportAttributes( sanitizedAttributes );
60+
}
61+
62+
// If httpMethod is 'POST', send the attributes in the request body instead of the URL.
63+
// This allows sending a larger attributes object than in a GET request, where the attributes are in the URL.
64+
const isPostRequest = 'POST' === httpMethod;
65+
const urlAttributes = isPostRequest ? null : sanitizedAttributes;
66+
const path = rendererPath( block, urlAttributes, urlQueryArgs );
67+
const body = isPostRequest
68+
? JSON.stringify( { attributes: sanitizedAttributes ?? null } )
69+
: undefined;
13970

140-
// When the component unmounts, set isMountedRef to false. This will
141-
// let the async fetch callbacks know when to stop.
14271
useEffect( () => {
143-
isMountedRef.current = true;
72+
const controller = new AbortController();
73+
const debouncedFetch = debounce(
74+
function () {
75+
{
76+
setResponse( { status: 'loading' } );
77+
78+
apiFetch( {
79+
path,
80+
method: isPostRequest ? 'POST' : 'GET',
81+
body,
82+
headers: {
83+
'Content-Type': 'application/json',
84+
},
85+
signal: controller.signal,
86+
} )
87+
.then( ( res ) => {
88+
setResponse( {
89+
status: 'success',
90+
html: res ? res.rendered : '',
91+
} );
92+
} )
93+
.catch( ( error ) => {
94+
// The request was aborted, do not update the response.
95+
if ( error.name === 'AbortError' ) {
96+
return;
97+
}
98+
99+
setResponse( {
100+
status: 'error',
101+
error: error.message,
102+
} );
103+
} )
104+
.finally( () => {
105+
// Debounce requests after first fetch.
106+
shouldDebounceRef.current = true;
107+
} );
108+
}
109+
},
110+
shouldDebounceRef.current ? 500 : 0
111+
);
112+
113+
debouncedFetch();
114+
144115
return () => {
145-
isMountedRef.current = false;
116+
controller.abort();
117+
debouncedFetch.cancel();
146118
};
147-
}, [] );
148-
149-
useEffect( () => {
150-
// Don't debounce the first fetch. This ensures that the first render
151-
// shows data as soon as possible.
152-
if ( prevArgs === undefined ) {
153-
fetchData();
154-
} else if ( ! fastDeepEqual( prevArgs, args ) ) {
155-
debouncedFetchData();
156-
}
157-
} );
119+
}, [ path, isPostRequest, body ] );
158120

159-
return { response, isLoading };
121+
return response;
160122
}

packages/server-side-render/src/server-side-render.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { RawHTML, useEffect, useState } from '@wordpress/element';
4+
import { RawHTML, useEffect, useState, useRef } from '@wordpress/element';
55
import { __, sprintf } from '@wordpress/i18n';
66
import { Placeholder, Spinner } from '@wordpress/components';
77

@@ -18,11 +18,11 @@ function DefaultEmptyResponsePlaceholder( { className } ) {
1818
);
1919
}
2020

21-
function DefaultErrorResponsePlaceholder( { response, className } ) {
21+
function DefaultErrorResponsePlaceholder( { message, className } ) {
2222
const errorMessage = sprintf(
2323
// translators: %s: error message describing the problem
2424
__( 'Error loading block: %s' ),
25-
response.errorMsg
25+
message
2626
);
2727
return <Placeholder className={ className }>{ errorMessage }</Placeholder>;
2828
}
@@ -66,6 +66,7 @@ function DefaultLoadingResponsePlaceholder( { children, isLoading } ) {
6666
}
6767

6868
export default function ServerSideRender( props ) {
69+
const prevHTMLtRef = useRef( '' );
6970
const {
7071
className,
7172
EmptyResponsePlaceholder = DefaultEmptyResponsePlaceholder,
@@ -74,29 +75,34 @@ export default function ServerSideRender( props ) {
7475
...restProps
7576
} = props;
7677

77-
const { response, isLoading } = useServerSideRender( restProps );
78+
const { html, status, error } = useServerSideRender( restProps );
7879

79-
const hasResponse = !! response;
80-
const hasEmptyResponse = response === '';
81-
const hasError = !! response?.error;
80+
// Store the previous successful HTML response to show while loading.
81+
useEffect( () => {
82+
if ( html ) {
83+
prevHTMLtRef.current = html;
84+
}
85+
}, [ html ] );
8286

83-
if ( isLoading ) {
87+
if ( status === 'loading' ) {
8488
return (
85-
<LoadingResponsePlaceholder { ...props } isLoading={ isLoading }>
86-
{ hasResponse && ! hasError && (
87-
<RawHTML className={ className }>{ response }</RawHTML>
89+
<LoadingResponsePlaceholder { ...props } isLoading>
90+
{ prevHTMLtRef.current && (
91+
<RawHTML className={ className }>
92+
{ prevHTMLtRef.current }
93+
</RawHTML>
8894
) }
8995
</LoadingResponsePlaceholder>
9096
);
9197
}
9298

93-
if ( hasEmptyResponse || ! hasResponse ) {
99+
if ( status === 'success' && ! html ) {
94100
return <EmptyResponsePlaceholder { ...props } />;
95101
}
96102

97-
if ( hasError ) {
98-
return <ErrorResponsePlaceholder response={ response } { ...props } />;
103+
if ( status === 'error' ) {
104+
return <ErrorResponsePlaceholder message={ error } { ...props } />;
99105
}
100106

101-
return <RawHTML className={ className }>{ response }</RawHTML>;
107+
return <RawHTML className={ className }>{ html }</RawHTML>;
102108
}

0 commit comments

Comments
 (0)