|
1 |
| -/** |
2 |
| - * External dependencies |
3 |
| - */ |
4 |
| -import fastDeepEqual from 'fast-deep-equal/es6'; |
5 |
| - |
6 | 1 | /**
|
7 | 2 | * WordPress dependencies
|
8 | 3 | */
|
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'; |
17 | 6 | import apiFetch from '@wordpress/api-fetch';
|
18 | 7 | import { addQueryArgs } from '@wordpress/url';
|
19 | 8 | import { __experimentalSanitizeBlockAttributes } from '@wordpress/blocks';
|
@@ -50,111 +39,84 @@ export function removeBlockSupportAttributes( attributes ) {
|
50 | 39 | }
|
51 | 40 |
|
52 | 41 | 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 ); |
137 | 44 |
|
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; |
139 | 70 |
|
140 |
| - // When the component unmounts, set isMountedRef to false. This will |
141 |
| - // let the async fetch callbacks know when to stop. |
142 | 71 | 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 | + |
144 | 115 | return () => {
|
145 |
| - isMountedRef.current = false; |
| 116 | + controller.abort(); |
| 117 | + debouncedFetch.cancel(); |
146 | 118 | };
|
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 ] ); |
158 | 120 |
|
159 |
| - return { response, isLoading }; |
| 121 | + return response; |
160 | 122 | }
|
0 commit comments