Skip to content

Commit

Permalink
Refactor ServerSideRender to use React hooks. (#28297)
Browse files Browse the repository at this point in the history
* Refactor ServerSideRender to use hooks.

* Rename fetch to fetchData.
  • Loading branch information
ZebulanStanphill committed Mar 12, 2021
1 parent 786bfa7 commit 09671e3
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 111 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/server-side-render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/data": "file:../data",
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
Expand Down
201 changes: 90 additions & 111 deletions packages/server-side-render/src/server-side-render.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/**
* External dependencies
*/
import { isEqual, debounce } from 'lodash';
import { isEqual } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, RawHTML } from '@wordpress/element';
import { useDebounce, usePrevious } from '@wordpress/compose';
import { RawHTML, useEffect, useRef, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
Expand All @@ -21,45 +22,55 @@ export function rendererPath( block, attributes = null, urlQueryArgs = {} ) {
} );
}

export class ServerSideRender extends Component {
constructor( props ) {
super( props );
this.state = {
response: null,
};
}
function DefaultEmptyResponsePlaceholder( { className } ) {
return (
<Placeholder className={ className }>
{ __( 'Block rendered as empty.' ) }
</Placeholder>
);
}

componentDidMount() {
this.isStillMounted = true;
this.fetch( this.props );
// Only debounce once the initial fetch occurs to ensure that the first
// renders show data as soon as possible.
this.fetch = debounce( this.fetch, 500 );
}
function DefaultErrorResponsePlaceholder( { response, className } ) {
const errorMessage = sprintf(
// translators: %s: error message describing the problem
__( 'Error loading block: %s' ),
response.errorMsg
);
return <Placeholder className={ className }>{ errorMessage }</Placeholder>;
}

componentWillUnmount() {
this.isStillMounted = false;
}
function DefaultLoadingResponsePlaceholder( { className } ) {
return (
<Placeholder className={ className }>
<Spinner />
</Placeholder>
);
}

componentDidUpdate( prevProps ) {
if ( ! isEqual( prevProps, this.props ) ) {
this.fetch( this.props );
}
}
export default function ServerSideRender( props ) {
const {
attributes,
block,
className,
httpMethod = 'GET',
urlQueryArgs,
EmptyResponsePlaceholder = DefaultEmptyResponsePlaceholder,
ErrorResponsePlaceholder = DefaultErrorResponsePlaceholder,
LoadingResponsePlaceholder = DefaultLoadingResponsePlaceholder,
} = props;

const isMountedRef = useRef( true );
const fetchRequestRef = useRef();
const [ response, setResponse ] = useState( null );
const prevProps = usePrevious( props );

fetch( props ) {
if ( ! this.isStillMounted ) {
function fetchData() {
if ( ! isMountedRef.current ) {
return;
}
if ( null !== this.state.response ) {
this.setState( { response: null } );
if ( null !== response ) {
setResponse( null );
}
const {
block,
attributes = null,
httpMethod = 'GET',
urlQueryArgs = {},
} = props;

const sanitizedAttributes =
attributes &&
Expand All @@ -68,105 +79,73 @@ export class ServerSideRender extends Component {
// If httpMethod is 'POST', send the attributes in the request body instead of the URL.
// This allows sending a larger attributes object than in a GET request, where the attributes are in the URL.
const isPostRequest = 'POST' === httpMethod;
const urlAttributes = isPostRequest ? null : sanitizedAttributes;
const urlAttributes = isPostRequest
? null
: sanitizedAttributes ?? null;
const path = rendererPath( block, urlAttributes, urlQueryArgs );
const data = isPostRequest ? { attributes: sanitizedAttributes } : null;
const data = isPostRequest
? { attributes: sanitizedAttributes ?? null }
: null;

// Store the latest fetch request so that when we process it, we can
// check if it is the current request, to avoid race conditions on slow networks.
const fetchRequest = ( this.currentFetchRequest = apiFetch( {
const fetchRequest = ( fetchRequestRef.current = apiFetch( {
path,
data,
method: isPostRequest ? 'POST' : 'GET',
} )
.then( ( response ) => {
.then( ( fetchResponse ) => {
if (
this.isStillMounted &&
fetchRequest === this.currentFetchRequest &&
response
isMountedRef.current &&
fetchRequest === fetchRequestRef.current &&
fetchResponse
) {
this.setState( { response: response.rendered } );
setResponse( fetchResponse.rendered );
}
} )
.catch( ( error ) => {
if (
this.isStillMounted &&
fetchRequest === this.currentFetchRequest
isMountedRef.current &&
fetchRequest === fetchRequestRef.current
) {
this.setState( {
response: {
error: true,
errorMsg: error.message,
},
setResponse( {
error: true,
errorMsg: error.message,
} );
}
} ) );

return fetchRequest;
}

render() {
const response = this.state.response;
const {
className,
EmptyResponsePlaceholder,
ErrorResponsePlaceholder,
LoadingResponsePlaceholder,
} = this.props;

if ( response === '' ) {
return (
<EmptyResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
} else if ( ! response ) {
return (
<LoadingResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
} else if ( response.error ) {
return (
<ErrorResponsePlaceholder
response={ response }
{ ...this.props }
/>
);
const debouncedFetchData = useDebounce( fetchData, 500 );

// When the component unmounts, set isMountedRef to false. This will
// let the async fetch callbacks know when to stop.
useEffect(
() => () => {
isMountedRef.current = false;
},
[]
);

useEffect( () => {
// Don't debounce the first fetch. This ensures that the first render
// shows data as soon as possible
if ( prevProps === undefined ) {
fetchData();
} else if ( ! isEqual( prevProps, props ) ) {
debouncedFetchData();
}
} );

return (
<RawHTML key="html" className={ className }>
{ response }
</RawHTML>
);
if ( response === '' ) {
return <EmptyResponsePlaceholder { ...props } />;
} else if ( ! response ) {
return <LoadingResponsePlaceholder { ...props } />;
} else if ( response.error ) {
return <ErrorResponsePlaceholder response={ response } { ...props } />;
}
}

ServerSideRender.defaultProps = {
EmptyResponsePlaceholder: ( { className } ) => (
<Placeholder className={ className }>
{ __( 'Block rendered as empty.' ) }
</Placeholder>
),
ErrorResponsePlaceholder: ( { response, className } ) => {
const errorMessage = sprintf(
// translators: %s: error message describing the problem
__( 'Error loading block: %s' ),
response.errorMsg
);
return (
<Placeholder className={ className }>{ errorMessage }</Placeholder>
);
},
LoadingResponsePlaceholder: ( { className } ) => {
return (
<Placeholder className={ className }>
<Spinner />
</Placeholder>
);
},
};

export default ServerSideRender;
return <RawHTML className={ className }>{ response }</RawHTML>;
}

0 comments on commit 09671e3

Please sign in to comment.