How to prevent SubFormState storage leaking into parent?
So far, I have solved it by using #array_parents for getting/setting values into form state storage in plugin forms since there cannot be a key collision in parent form this way.

Small issue with this is that I have to provide the whole parents in there. So if the plugin form is not flat, I have to add all those additional parents in there since they will appear in the ...

... additionally to this in ajax plugin form submit handlers and ajax callbacks I have to get the the "root" element(ie. the form element for the plugin) by detecting #array_parents on the trigger and extracting the proper form element by #array_parents and extracting the #array_parents from it to get to the data in the storage.

Overall it is a massiv DX fail. I have a ***load of plugins and to bulletproof them this way will take a lot of unnecessary time now.

I was thinking about extending the SubformState as KeyedSubformState where the state would "hide" the plugin's storage under provided key so the parent form would control this and be responsible for avoiding key collision and the plugin form doesn't have to be altered in any way(since values are already taken care of) but I have decided not to do it this way since it feels like adding additional layer is not a good idea in the long run...also it won' work for ajax submit handlers since they receive the full form and state and not the subform element and state.

If anyone has a better solution, I'm all ears. But for now it seems plugins are treated like homeles people :D



Also, for the love of god, do not use #parents in plugin form. Or you'll be leaking values into the parent form. Overall, just make the plugin form as dumb as possible. Flat and no ajax, just to be safe.



This is, currently working, plugin with ajax.

/**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $ajax = [
      'wrapper' =&gt; Html::getId($this-&gt;getPluginId() . '-countries-ajax-wrapper'),
      'callback' =&gt; [get_called_class(), 'ajaxRebuildCountries'],
      'progress' =&gt; [
        'type' =&gt; 'throbber'
      ]
    ];

    $form['allowed_countries'] = [
      '#type' =&gt; 'fieldset',
      '#title' =&gt; $this-&gt;t('Allowed countries'),
      '#prefix' =&gt; '&lt;div id="' . $ajax['wrapper'] . '"&gt;',
      '#suffix' =&gt; '&lt;/div&gt;'
    ];

    $country_list = $this-&gt;countryManager-&gt;getList();

    // @see https://drupal.stackexchange.com/questions/243812
    // @see https://www.drupal.org/node/2902121
    $state = $form['#array_parents'];
    $state[] = 'allowed_countries';
    $state[] = 'countries';

    if ($form_state-&gt;has($state) === FALSE) {
      $form_state-&gt;set($state, $this-&gt;getSetting('countries'));
    }
    $countries = $form_state-&gt;get($state);

    $form['allowed_countries']['countries'] = [
      '#type' =&gt; 'table',
      '#default_value' =&gt; [],
      '#empty' =&gt; $this-&gt;t('No countries have been added yet.')
    ];

    foreach ($countries AS $iso =&gt; $level_1) {
      $row = [];

      $row['iso'] = [
        '#markup' =&gt; $iso
      ];

      $row['label'] = [
        '#markup' =&gt; $country_list[$iso]
      ];

      $row['level_1'] = [
        '#type' =&gt; 'value',
        '#value' =&gt; []
      ];

      if ($level_1_list = Address::hasLevel1($iso)) {
        $row['level_1'] = [
          '#type' =&gt; 'select',
          '#title' =&gt; Address::getLevel1Label($iso),
          '#options' =&gt; $level_1_list,
          '#default_value' =&gt; $level_1,
          '#multiple' =&gt; TRUE
        ];
      }

      $row['delete'] = [
        '#type' =&gt; 'submit',
        '#value' =&gt; $this-&gt;t('Delete'),
        '#submit' =&gt; [[get_called_class(), 'deleteCountrySubmit']],
        '#name' =&gt; 'delete_country_' . $iso,
        '#ajax' =&gt; $ajax
      ];

      $form['allowed_countries']['countries'][$iso] = $row;
    }

    $form['allowed_countries']['add_country'] = [
      'country' =&gt; [
        '#type' =&gt; 'select',
        '#title' =&gt; $this-&gt;t('Add country'),
        '#options' =&gt; array_diff_key($country_list, $countries),
        '#ajax' =&gt; $ajax + ['trigger_as' =&gt; ['name' =&gt; 'add_country_submit']],
        '#empty_value' =&gt; '',
        '#empty_option' =&gt; t('- Select -')
      ],
      'submit' =&gt; [
        '#type' =&gt; 'submit',
        '#value' =&gt; $this-&gt;t('Add country'),
        '#submit' =&gt; [[get_called_class(), 'addCountrySubmit']],
        '#name' =&gt; 'add_country_submit',
        '#ajax' =&gt; $ajax,
        '#attributes' =&gt; [
          'class' =&gt; ['js-hide']
        ]
      ]
    ];

    return $form;
  }

  /**
   * Form submit handler for attaching a new allowed country.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   */
  public static function addCountrySubmit(array $form, FormStateInterface $form_state) {
    $element = static::getFormElement($form, $form_state);

    // @see https://drupal.stackexchange.com/questions/243812
    // @see https://www.drupal.org/node/2902121
    $get_country = array_merge($element['#parents'], ['add_country', 'country']);
    $get_countries = array_merge($element['#parents'], ['countries']);
    $set_state = array_merge($element['#array_parents'], ['countries']);

    $country = $form_state-&gt;getValue($get_country);
    $countries = $form_state-&gt;getValue($get_countries);
    $countries[$country] = [];

    ksort($countries);

    $form_state-&gt;set($set_state, $countries);

    $form_state-&gt;setRebuild();
  }

  /**
   * Form submit handler for removing allowed country.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   */
  public static function deleteCountrySubmit(array $form, FormStateInterface $form_state) {
    $element = static::getFormElement($form, $form_state);

    $country = substr($form_state-&gt;getTriggeringElement()['#name'], strlen('delete_country_'));

    // @see https://drupal.stackexchange.com/questions/243812
    // @see https://www.drupal.org/node/2902121
    $get_countries = array_merge($element['#parents'], ['countries']);
    $set_state = array_merge($element['#array_parents'], ['countries']);

    $countries = $form_state-&gt;getValue($get_countries);
    unset($countries[$country]);

    ksort($countries);

    $form_state-&gt;set($set_state, $countries);

    $form_state-&gt;setRebuild();
  }

  /**
   * Ajax callback for rebuilding the allowed countries form elements.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *
   * @return mixed
   */
  public static function ajaxRebuildCountries(array $form, FormStateInterface $form_state) {
    return static::getFormElement($form, $form_state);
  }

  /**
   * Helper method to retrieve the form element containing the plugin
   * configuration "form" from the parent form for ajax calls since
   * there is no SubformState involved and we are getting the whole
   * parent form.
   *
   * @param array $form
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *
   * @return mixed
   */
  public static function getFormElement(array $form, FormStateInterface $form_state) {
    $trigger_parents = $form_state-&gt;getTriggeringElement()['#array_parents'];
    $key = array_search('allowed_countries', $trigger_parents);
    return NestedArray::getValue($form, array_slice($trigger_parents, 0, $key + 1));
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &amp;$form, FormStateInterface $form_state) {
    $values = $form_state-&gt;getValue(['allowed_countries', 'countries']);

    // Fix Level 1 administrative area.
    $values = array_map(function($value) {
      if (is_array($value) &amp;&amp; isset($value['level_1'])) {
        return $value['level_1'];
      }
      return $value;
    }, $values);

    $form_state-&gt;setValues(['countries' =&gt; $values]);
    parent::submitConfigurationForm($form, $form_state);
  }