6

I built a custom Gutenberg block to show ONE post of a custom post type called story. Most things are finally working fine, story shows up as wanted via ServerSideRendering. To select the story-to-show there is a List of radio buttons in the sidebar.

What I want: - after choosing a story from the radio buttons, I want the shown story to refresh. From some examples I believe this gets solved with withSelect or useSelect or something like that. But I can't wrap my head around it, I don't understand how to use them for my case.

When the story_id of the edit function gets updated, the Story object needs to get rendered again with that new story_id.

Here's the code:

/**
* Story-Block: osf-blocks-story
*
* Gives back a rendered story (list of images with text & title) after selecting from the left block sidebar
*
* based on create-guten-block
*/

import './style.scss';
import './editor.scss';

import { registerBlockType } from '@wordpress/blocks';
import { ServerSideRender, RadioControl, PanelBody } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';
import { Component, Fragment } from '@wordpress/element';
import { useSelect, withSelect } from '@wordpress/data';


let $ = jQuery;

// short for console.log
function cl(words) { console.log('drx-osf-story, block.js: ' + words); }

// to get more object infos than console.log(obj + 'some comment');
function ol(object, words='') {
   cl(words + ': \\/');
   console.log(object);
}

// Represents a single story, i.e. a collection of parts with image, short text and possibly a title
class Story extends Component {
   constructor(props) {
       super(props);
       this.state = {
           story_id: props.story_id
       }
   }

   render() {
       return (
           <ServerSideRender
               block="osf-blocks/story"
               attributes={this.state}
           />
       )
   }
}

// Represents a list of radiobuttons to select one of all stories
class StoriesListControl extends Component {

   constructor(props) {
       super(props);
       this.state = {
           changeHandler: props.changeHandler,
           story_id: props.story_id,
           options: props.options
       };
   }

   render() {
       return (
           <InspectorControls>
               <PanelBody title='Story auswählen:'>
                   <RadioControl
                       label="Wähle, welche Story hier eingefügt werden soll."
                       help="Die Story muss vorher über das Story-Interface angelegt worden sein."
                       selected={ this.state.story_id }
                       options={ this.state.options }
                       onChange={ this.state.changeHandler }
                   />
               </PanelBody>
           </InspectorControls>
       );
   }
};


// Registering the block
registerBlockType('osf-blocks/story', {

   title: 'Story' ,
   icon: 'images-alt2',
   category: 'widgets',
   attributes: {
       story_id: {
           type: 'string'
       }
   },
   keywords: [
       'osf-blocks-story',
       'Story',
       'Stories'
   ],


   edit: (props) => {

       // what to do with withSelect/useSelect here??

       const {
           attributes: { story_id },
           setAttributes
           } = props;

       // called, if a radiobutton gets selected
       const changeHandler = (new_story_id) => {
           setAttributes({ story_id: new_story_id });
       };

       // fetching the list of all stories (custom post type) from server
       const getStoriesList = () => {
           let answer;
           $.ajax({
               url: '/wp-admin/admin-ajax.php',
               type: 'POST',
               data: {
                   action: 'stories_list'
               },
               async: false,
               timeout: 3000 // ms
           }).done((rspns) => {
               // removing trailing 0  - what does it mean?
               answer = { status: 'success', result: JSON.parse(rspns.substr(0, rspns.length - 1)) };
           }).fail((xhr, status) => {
               cl('request failed: ' + status);
               ol(xhr);
               answer = { status:'fail',  log: status }
           });
           return answer;
       };

       // tie it all together
       let response = getStoriesList();
       let output;

       if ( 'success' == response.status) {
       // AJAX for storieslist succeeded

           ol(response, 'response');

           output = (
               <Fragment>
                   <StoriesListControl
                       changeHandler={ changeHandler }
                       story_id={ story_id }
                       options={ response.result }
                   />
                   <div className="story-container">
                       <div id={ "story" + story_id } className="story-preview container">
                           <Story story_id={ story_id } />
                       </div>
                   </div>
               </Fragment>
           );

       }
       else if ('fail' == response.status) {
       // AJAX failed
           output = (
               <Fragment>
                   <div className="story-container">
                       <p>No connection to the server: { response.log }</p>
                   </div>
               </Fragment>
           );
       }

       return output;
   },

   save: (props) => {
       return null;
   }

});

1 Answer 1

1

In this instance, you are treating the asynchronous jQuery AJAX call as a synchronous function, resulting in getStoriesList() consistently returning undefined as the function returns immediately after dispatching the HTTP request; trying to access response.status should be consistently throwing ReferenceError: response is not defined.

In almost all cases it is more ideal to access data through the selectors provided on Gutenberg's various data-stores - many selectors will asynchronously acquire data from the REST API if it is missing or uninitialized in the store, then update the respective state, and automatically trigger updates for every component which has subscribed to changes to that state using withSelect() or useSelect().

In circumventing Gutenberg's data-flow here, you are unable to update the component when new data comes in using useSelect() or withSelect() because the response from your AJAX handler does not trigger a state update in the stores, and thus the stores will not notify any subscribed functions or components. The current implementation also results in an HTTP request being dispatched every time the component is rendered, which is unecessary.

In this case, the getStoriesList() function and associated AJAX handler returning a list of CPT posts should be succinctly replaced with the getEntityRecords() selector on the core data store. We can get access to the core data store's selectors via the object returned by select( 'core' ). Wrapping that access up in a useSelect() hook creates a subscription to the core store, re-rendering edit() automatically whenever the data being selected changes in state.

Further, the second argument of useSelect() is an array of dependencies. By adding story_id to it, we can cache the post list and only retrieve a new one when story_id changes.

// Registering the block
registerBlockType('osf-blocks/story', {
  //...

  edit: (props) => {
    const {
      attributes: { story_id },
      setAttributes
    } = props;

    const stories = useSelect(
      select => select( 'core' ).getEntityRecords(
        'postType',
        'story',
        { id: story_id }
      ),
      [ story_id ]
    );

    // called, if a radiobutton gets selected
    const setStoryID = ( story_id ) => setAttributes( { story_id } );

    return (
      <Fragment>
        <StoriesListControl
          changeHandler={ setStoryID }
          story_id={ story_id }
          options={ stories }
        />
        <div className="story-container">
          <div id={ `story${ story_id }` } className="story-preview-container">
            <Story story_id={ story_id } />
          </div>
        </div>
      </Fragment>
    );
  }

  // ...
});
2
  • Although this question is very old I will try this! Thank you for the insight.
    – ejoo
    Jun 23, 2021 at 16:36
  • Might be some bugs in there - I haven't tested it myself. But thought the question should get an answer/explanation in any scenario, even though it was likely you no longer required one :)
    – bosco
    Jun 23, 2021 at 16:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.