diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 9cb0b0d..79a3abb 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -16,6 +16,8 @@
  */
 class FormState implements FormStateInterface {
 
+  use FormStateValuesTrait;
+
   /**
    * Tracks if any errors have been set on any form.
    *
@@ -195,21 +197,6 @@ class FormState implements FormStateInterface {
   protected $no_cache;
 
   /**
-   * An associative array of values submitted to the form.
-   *
-   * The validation functions and submit functions use this array for nearly all
-   * their decision making. (Note that #tree determines whether the values are a
-   * flat array or an array whose structure parallels the $form array. See the
-   * @link forms_api_reference.html Form API reference @endlink for more
-   * information.)
-   *
-   * This property is uncacheable.
-   *
-   * @var array
-   */
-  protected $values = array();
-
-  /**
    * An associative array of form value keys to be removed by cleanValues().
    *
    * Any values that are temporary but must still be displayed as values in
@@ -966,74 +953,6 @@ public function setUserInput(array $user_input) {
   /**
    * {@inheritdoc}
    */
-  public function &getValues() {
-    return $this->values;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function &getValue($key, $default = NULL) {
-    $exists = NULL;
-    $value = &NestedArray::getValue($this->getValues(), (array) $key, $exists);
-    if (!$exists) {
-      $value = $default;
-    }
-    return $value;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setValues(array $values) {
-    $this->values = $values;
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setValue($key, $value) {
-    NestedArray::setValue($this->getValues(), (array) $key, $value, TRUE);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function unsetValue($key) {
-    NestedArray::unsetValue($this->getValues(), (array) $key);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function hasValue($key) {
-    $exists = NULL;
-    $value = NestedArray::getValue($this->getValues(), (array) $key, $exists);
-    return $exists && isset($value);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function isValueEmpty($key) {
-    $exists = NULL;
-    $value = NestedArray::getValue($this->getValues(), (array) $key, $exists);
-    return !$exists || empty($value);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setValueForElement(array $element, $value) {
-    return $this->setValue($element['#parents'], $value);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function setResponse(Response $response) {
     $this->response = $response;
     return $this;
@@ -1290,4 +1209,11 @@ protected function moduleLoadInclude($module, $type, $name = NULL) {
     return \Drupal::moduleHandler()->loadInclude($module, $type, $name);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormStateForElement(array $element) {
+    return SubFormState::createForFormElement($this, $element);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
index fd46f70..761e4d2 100644
--- a/core/lib/Drupal/Core/Form/FormStateInterface.php
+++ b/core/lib/Drupal/Core/Form/FormStateInterface.php
@@ -1098,4 +1098,14 @@ public function addCleanValueKey($key);
    */
   public function cleanValues();
 
+  /**
+   * Gets the form state for a form element.
+   *
+   * @param mixed[] $element
+   *   The form element for which to get the form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   */
+  public function getFormStateForElement(array $element);
+
 }
diff --git a/core/lib/Drupal/Core/Form/FormStateValuesTrait.php b/core/lib/Drupal/Core/Form/FormStateValuesTrait.php
new file mode 100644
index 0000000..dafadef
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormStateValuesTrait.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormStateValuesTrait.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Provides methods to manage form state values.
+ *
+ * @see \Drupal\Core\Form\FormStateInterface
+ *
+ * @ingroup form_api
+ */
+trait FormStateValuesTrait {
+
+  /**
+   * An associative array of values submitted to the form.
+   *
+   * The validation functions and submit functions use this array for nearly all
+   * their decision making. (Note that #tree determines whether the values are a
+   * flat array or an array whose structure parallels the $form array. See the
+   * @link forms_api_reference.html Form API reference @endlink for more
+   * information.)
+   *
+   * This property is uncacheable.
+   *
+   * @var array
+   */
+  protected $values = array();
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::getValues()
+   */
+  public function &getValues() {
+    return $this->values;
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::getValue()
+   */
+  public function &getValue($key, $default = NULL) {
+    $exists = NULL;
+    $value = &NestedArray::getValue($this->getValues(), (array) $key, $exists);
+    if (!$exists) {
+      $value = $default;
+    }
+    return $value;
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::setValues()
+   */
+  public function setValues(array $values) {
+    $existing_values = &$this->getValues();
+    $existing_values = $values;
+    return $this;
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::setValue()
+   */
+  public function setValue($key, $value) {
+    NestedArray::setValue($this->getValues(), (array) $key, $value, TRUE);
+    return $this;
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::unsetValue()
+   */
+  public function unsetValue($key) {
+    NestedArray::unsetValue($this->getValues(), (array) $key);
+    return $this;
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::hasValue()
+   */
+  public function hasValue($key) {
+    $exists = NULL;
+    $value = NestedArray::getValue($this->getValues(), (array) $key, $exists);
+    return $exists && isset($value);
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::isValueEmpty()
+   */
+  public function isValueEmpty($key) {
+    $exists = NULL;
+    $value = NestedArray::getValue($this->getValues(), (array) $key, $exists);
+    return !$exists || empty($value);
+  }
+
+  /**
+   * @see \Drupal\Core\Form\FormStateInterface::setValueForElement()
+   */
+  public function setValueForElement(array $element, $value) {
+    return $this->setValue($element['#parents'], $value);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/SubFormState.php b/core/lib/Drupal/Core/Form/SubFormState.php
new file mode 100644
index 0000000..dbdb7b9
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/SubFormState.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\SubFormState.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Render\Element;
+
+/**
+ * Stores information about the state of a subform.
+ */
+class SubFormState extends FormStateDecoratorBase {
+
+  use FormStateValuesTrait;
+
+  /**
+   * The root #array_parents value of the subform.
+   *
+   * @var string[]
+   */
+  protected $arrayParents = [];
+
+  /**
+   * The root #parents value of the subform.
+   *
+   * @var string[]
+   */
+  protected $parents = [];
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
+   *   The parent form state.
+   * @param string[] $array_parents
+   *   The root #array_parents value of the subset of the form.
+   * @param string[] $parents
+   *   The root #parents value of the subset of the form.
+   */
+  public function __construct(FormStateInterface $parent_form_state, array $array_parents, array $parents) {
+    parent::__construct($parent_form_state);
+    $this->arrayParents = $array_parents;
+    $this->parents = $parents;
+  }
+
+  /**
+   * Creates a new instance based on a form element.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
+   *   The parent form state.
+   * @param mixed[] $element
+   *   The element to create the sub form state for.
+   *
+   * @return static
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the form element does not wrap other form elements, or if it
+   *   does not have the necessary Form API properties assigned.
+   */
+  public static function createForFormElement(FormStateInterface $parent_form_state, array $element) {
+    // Make sure the element contains the required properties.
+    $children = Element::children($element);
+    if (empty($children)) {
+      throw new \InvalidArgumentException('nope');
+    }
+    $required_keys = ['#array_parents', '#parents'];
+    foreach ($required_keys as $required_key) {
+      if (!array_key_exists($required_key, $element) || !is_array($element[$required_key])) {
+        throw new \InvalidArgumentException(sprintf('$element must contain the %s key. Try calling this method from a #process callback instead.', $required_key));
+      }
+    }
+
+    return new static($parent_form_state, $element['#parents'], $element['#array_parents']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getValues() {
+    $exists = NULL;
+    $values = &NestedArray::getValue(parent::getValues(), $this->parents, $exists);
+    if (!$exists) {
+      $values = [];
+    }
+    elseif (!is_array($values)) {
+      throw new \UnexpectedValueException('Drilled down too far! This violates the interface.');
+    }
+
+
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormStateForElement(array $element) {
+    return static::createForFormElement($this, $element);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php
index 9fb3ad5..cbd1d4b 100644
--- a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php
+++ b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php
@@ -33,7 +33,9 @@
    * @param array $form
    *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
+   *   The current state of the form. Calling code should pass on a sub form
+   *   state created through
+   *   \Drupal\Core\Form\FormStateInterface::createFormFormElement().
    *
    * @return array
    *   The form structure.
@@ -46,7 +48,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
    * @param array $form
    *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
+   *   The current state of the form. Calling code should pass on a sub form
+   *   state created through
+   *   \Drupal\Core\Form\FormStateInterface::createFormFormElement().
    */
   public function validateConfigurationForm(array &$form, FormStateInterface $form_state);
 
@@ -56,7 +60,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
    * @param array $form
    *   An associative array containing the structure of the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
+   *   The current state of the form. Calling code should pass on a sub form
+   *   state created through
+   *   \Drupal\Core\Form\FormStateInterface::createFormFormElement().
    */
   public function submitConfigurationForm(array &$form, FormStateInterface $form_state);
 
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index 8c13bbb..2720a7f 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -285,11 +285,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for validation.
-    $settings = (new FormState())->setValues($form_state->getValue('settings'));
-    // Call the plugin validate handler.
-    $this->entity->getPlugin()->validateConfigurationForm($form, $settings);
-    // Update the original form values.
-    $form_state->setValue('settings', $settings->getValues());
+    $this->entity->getPlugin()->validateConfigurationForm($form, $form_state->getFormStateForElement($form['settings']));
     $this->validateVisibility($form, $form_state);
   }
 
@@ -313,11 +309,7 @@ protected function validateVisibility(array $form, FormStateInterface $form_stat
 
       // Allow the condition to validate the form.
       $condition = $form_state->get(['conditions', $condition_id]);
-      $condition_values = (new FormState())
-        ->setValues($values);
-      $condition->validateConfigurationForm($form, $condition_values);
-      // Update the original form values.
-      $form_state->setValue(['visibility', $condition_id], $condition_values->getValues());
+      $condition->validateConfigurationForm($form, $form_state->getFormStateForElement($form['visibility'][$condition_id]));
     }
   }
 
@@ -330,29 +322,21 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $entity = $this->entity;
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for submission.
-    // @todo Find a way to avoid this manipulation.
-    $settings = (new FormState())->setValues($form_state->getValue('settings'));
-
-    // Call the plugin submit handler.
-    $entity->getPlugin()->submitConfigurationForm($form, $settings);
-    // Update the original form values.
-    $form_state->setValue('settings', $settings->getValues());
+    $entity->getPlugin()->submitConfigurationForm($form, $form_state->getFormStateForElement($form['settings']));
 
     // Submit visibility condition settings.
     foreach ($form_state->getValue('visibility') as $condition_id => $values) {
       // Allow the condition to submit the form.
       $condition = $form_state->get(['conditions', $condition_id]);
-      $condition_values = (new FormState())
-        ->setValues($values);
-      $condition->submitConfigurationForm($form, $condition_values);
+      $condition->submitConfigurationForm($form, $form_state->getFormStateForElement($form['visibility'][$condition_id]));
+
       if ($condition instanceof ContextAwarePluginInterface) {
         $context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : [];
         $condition->setContextMapping($context_mapping);
       }
-      // Update the original form values.
-      $condition_configuration = $condition->getConfiguration();
-      $form_state->setValue(['visibility', $condition_id], $condition_configuration);
+
       // Update the visibility conditions on the block.
+      $condition_configuration = $condition->getConfiguration();
       $entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
     }
 
diff --git a/core/modules/image/src/Form/ImageEffectFormBase.php b/core/modules/image/src/Form/ImageEffectFormBase.php
index 7b7f25b..bc12fb0 100644
--- a/core/modules/image/src/Form/ImageEffectFormBase.php
+++ b/core/modules/image/src/Form/ImageEffectFormBase.php
@@ -8,8 +8,8 @@
 namespace Drupal\image\Form;
 
 use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormState;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubFormState;
 use Drupal\image\ConfigurableImageEffectInterface;
 use Drupal\image\ImageStyleInterface;
 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
@@ -31,7 +31,7 @@
   /**
    * The image effect.
    *
-   * @var \Drupal\image\ImageEffectInterface
+   * @var \Drupal\image\ImageEffectInterface|\Drupal\image\ConfigurableImageEffectInterface
    */
   protected $imageEffect;
 
@@ -108,10 +108,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ImageStyl
   public function validateForm(array &$form, FormStateInterface $form_state) {
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for validation.
-    $effect_data = (new FormState())->setValues($form_state->getValue('data'));
-    $this->imageEffect->validateConfigurationForm($form, $effect_data);
-    // Update the original form values.
-    $form_state->setValue('data', $effect_data->getValues());
+    $this->imageEffect->validateConfigurationForm($form, $form_state->getFormStateForElement($form['data']));
   }
 
   /**
@@ -122,10 +119,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
 
     // The image effect configuration is stored in the 'data' key in the form,
     // pass that through for submission.
-    $effect_data = (new FormState())->setValues($form_state->getValue('data'));
-    $this->imageEffect->submitConfigurationForm($form, $effect_data);
-    // Update the original form values.
-    $form_state->setValue('data', $effect_data->getValues());
+    $this->imageEffect->submitConfigurationForm($form, $form_state->getFormStateForElement($form['data']));
 
     $this->imageEffect->setWeight($form_state->getValue('weight'));
     if (!$this->imageEffect->getUuid()) {
