Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Podcast Player - make feed fetch call cancellable and cancel on Block removal. #15228

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions extensions/blocks/podcast-player/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import debugFactory from 'debug';
/**
* WordPress dependencies
*/
import { useState, useCallback, useEffect } from '@wordpress/element';
import { useState, useCallback, useEffect, useRef } from '@wordpress/element';
import {
Button,
ExternalLink,
Expand Down Expand Up @@ -42,6 +42,7 @@ import { queueMusic } from './icons/';
import { isAtomicSite, isSimpleSite } from '../../shared/site-type-utils';
import attributesValidation from './attributes';
import PodcastPlayer from './components/podcast-player';
import { makeCancellable } from './utils';
import { applyFallbackStyles } from '../../shared/apply-fallback-styles';

const DEFAULT_MIN_ITEMS = 1;
Expand Down Expand Up @@ -81,21 +82,30 @@ const PodcastPlayerEdit = ( {
const [ editedUrl, setEditedUrl ] = useState( url || '' );
const [ isEditing, setIsEditing ] = useState( false );
const [ feedData, setFeedData ] = useState( {} );
const cancellableFetch = useRef();

const fetchFeed = useCallback(
urlToFetch => {
const encodedURL = encodeURIComponent( urlToFetch );

apiFetch( {
path: '/wpcom/v2/podcast-player?url=' + encodedURL,
} ).then(
cancellableFetch.current = makeCancellable(
apiFetch( {
path: '/wpcom/v2/podcast-player?url=' + encodedURL,
} )
);

cancellableFetch.current.promise.then(
data => {
// Store feed data.
setFeedData( data );
},
err => {
error => {
if ( error && error.isCanceled ) {
debug( 'Block was unmounted during fetch', error );
return; // bail if canceled to avoid setting state
}
// Show error and allow to edit URL.
debug( 'feed error', err );
debug( 'feed error', error );
createErrorNotice(
__( "Your podcast couldn't be embedded. Please double check your URL.", 'jetpack' )
);
Expand All @@ -106,6 +116,14 @@ const PodcastPlayerEdit = ( {
[ createErrorNotice ]
);

useEffect( () => {
return () => {
if ( cancellableFetch && cancellableFetch.current && cancellableFetch.current.cancel ) {
cancellableFetch.current.cancel();
}
};
}, [] );

// Load RSS feed.
useEffect( () => {
// Clean state.
Expand Down
40 changes: 33 additions & 7 deletions extensions/blocks/podcast-player/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
* of compiled files used in the front-end.
*
* @example
* const className = getColorClassName( 'color', canvasPrimaryColor );
*
* @param {string} colorContextName Context/place where color is being used e.g: background, text etc...
* @param {string} colorSlug Slug of the color.
*
* @return {?string} String with the class corresponding to the color in the provided context.
* Returns undefined if either colorContextName or colorSlug are not provided.
* const className = getColorClassName( 'color', canvasPrimaryColor );
* @param colorContextName - Context/place where color is being used e.g: background, text etc...
* @param colorSlug - Slug of the color.
* @returns String with the class corresponding to the color in the provided context.
* Returns undefined if either colorContextName or colorSlug are not provided.
*/
export function getColorClassName( colorContextName, colorSlug ) {
if ( ! colorContextName || ! colorSlug ) {
Expand All @@ -20,3 +18,31 @@ export function getColorClassName( colorContextName, colorSlug ) {

return `has-${ colorSlug }-${ colorContextName }`;
}

/**
* Creates a wrapper around a promise which allows it to be programmatically
* cancelled. See: https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
*
* @example
* const somePromise = makeCancellable( fetch('http://wordpress.org') );
* somePromise.promise.then()
* @param promise - the Promise to make cancelable
* @returns Object containing original promise object and cancel method.
*/
export function makeCancellable( promise ) {
let hasCanceled_ = false;

const wrappedPromise = new Promise( ( resolve, reject ) => {
promise.then(
val => ( hasCanceled_ ? reject( { isCanceled: true } ) : resolve( val ) ),
error => ( hasCanceled_ ? reject( { isCanceled: true } ) : reject( error ) )
);
} );

return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
}