diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 9cb0b0d..c37269a 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 getFormStateForSubform(array $element) {
+    return SubFormState::createForSubform($element, $this);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php
new file mode 100644
index 0000000..4111b46
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php
@@ -0,0 +1,599 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormStateDecorator.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Decorates another form state.
+ */
+abstract class FormStateDecoratorBase implements FormStateInterface {
+
+  /**
+   * The decorated form state.
+   *
+   * @var \Drupal\Core\Form\FormStateInterface
+   */
+  protected $decoratedFormState;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFormState(array $form_state_additions) {
+    return $this->decoratedFormState->setFormState($form_state_additions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setAlwaysProcess($always_process = TRUE) {
+    return $this->decoratedFormState->setAlwaysProcess($always_process);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAlwaysProcess() {
+    return $this->decoratedFormState->getAlwaysProcess();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setButtons(array $buttons) {
+    return $this->decoratedFormState->setButtons($buttons);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getButtons() {
+    return $this->decoratedFormState->getButtons();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCached($cache = TRUE) {
+    return $this->decoratedFormState->setCached(TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCached() {
+    return $this->decoratedFormState->isCached();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function disableCache() {
+    return $this->decoratedFormState->disableCache();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setExecuted() {
+    return $this->decoratedFormState->setExecuted();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isExecuted() {
+    return $this->decoratedFormState->isExecuted();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setGroups(array $groups) {
+    return $this->decoratedFormState->setGroups($groups);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getGroups() {
+    return $this->decoratedFormState->getGroups();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setHasFileElement($has_file_element = TRUE) {
+    return $this->decoratedFormState->setHasFileElement($has_file_element);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasFileElement() {
+    return $this->decoratedFormState->hasFileElement();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLimitValidationErrors($limit_validation_errors) {
+    return $this->decoratedFormState->setLimitValidationErrors($limit_validation_errors);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLimitValidationErrors() {
+    return $this->decoratedFormState->getLimitValidationErrors();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setMethod($method) {
+    return $this->decoratedFormState->setMethod($method);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isMethodType($method_type) {
+    return $this->decoratedFormState->isMethodType($method_type);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRequestMethod($method) {
+    return $this->decoratedFormState->setRequestMethod($method);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationEnforced($must_validate = TRUE) {
+    return $this->decoratedFormState->setValidationEnforced($must_validate);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationEnforced() {
+    return $this->decoratedFormState->isValidationEnforced();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function disableRedirect($no_redirect = TRUE) {
+    return $this->decoratedFormState->disableRedirect($no_redirect);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRedirectDisabled() {
+    return $this->decoratedFormState->isRedirectDisabled();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProcessInput($process_input = TRUE) {
+    return $this->decoratedFormState->setProcessInput($process_input);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isProcessingInput() {
+    return $this->decoratedFormState->isProcessingInput();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProgrammed($programmed = TRUE) {
+    return $this->decoratedFormState->setProgrammed($programmed);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isProgrammed() {
+    return $this->decoratedFormState->isProgrammed();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setProgrammedBypassAccessCheck($programmed_bypass_access_check = TRUE) {
+    return $this->decoratedFormState->setProgrammedBypassAccessCheck($programmed_bypass_access_check);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isBypassingProgrammedAccessChecks() {
+    return $this->decoratedFormState->isBypassingProgrammedAccessChecks();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRebuildInfo(array $rebuild_info) {
+    return $this->decoratedFormState->setRebuildInfo($rebuild_info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRebuildInfo() {
+    return $this->decoratedFormState->getRebuildInfo();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addRebuildInfo($property, $value) {
+    return $this->decoratedFormState->addRebuildInfo($property, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setStorage(array $storage) {
+    return $this->decoratedFormState->setStorage($storage);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getStorage() {
+    return $this->decoratedFormState->getStorage();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubmitHandlers(array $submit_handlers) {
+    return $this->decoratedFormState->setSubmitHandlers($submit_handlers);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubmitHandlers() {
+    return $this->decoratedFormState->getSubmitHandlers();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setSubmitted() {
+    return $this->decoratedFormState->setSubmitted();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSubmitted() {
+    return $this->decoratedFormState->isSubmitted();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTemporary(array $temporary) {
+    return $this->decoratedFormState->setTemporary($temporary);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTemporary() {
+    return $this->decoratedFormState->getTemporary();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getTemporaryValue($key) {
+    return $this->decoratedFormState->getTemporaryValue($key);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTemporaryValue($key, $value) {
+    return $this->decoratedFormState->setTemporaryValue($key, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTemporaryValue($key) {
+    return $this->decoratedFormState->hasTemporaryValue($key);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTriggeringElement($triggering_element) {
+    return $this->decoratedFormState->setTriggeringElement($triggering_element);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getTriggeringElement() {
+    return $this->decoratedFormState->getTriggeringElement();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidateHandlers(array $validate_handlers) {
+    return $this->decoratedFormState->setValidateHandlers($validate_handlers);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValidateHandlers() {
+    return $this->decoratedFormState->getValidateHandlers();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationComplete($validation_complete = TRUE) {
+    return $this->decoratedFormState->setValidationComplete($validation_complete);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationComplete() {
+    return $this->decoratedFormState->isValidationComplete();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadInclude($module, $type, $name = NULL) {
+    return $this->decoratedFormState->loadInclude($module, $type, $name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableArray() {
+    return $this->decoratedFormState->getCacheableArray();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCompleteForm(array &$complete_form) {
+    return $this->decoratedFormState->setCompleteForm($complete_form);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getCompleteForm() {
+    return $this->decoratedFormState->getCompleteForm();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &get($property) {
+    return $this->decoratedFormState->get($property);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($property, $value) {
+    return $this->decoratedFormState->set($property, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function has($property) {
+    return $this->decoratedFormState->has($property);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setBuildInfo(array $build_info) {
+    return $this->decoratedFormState->setBuildInfo($build_info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBuildInfo() {
+    return $this->decoratedFormState->getBuildInfo();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addBuildInfo($property, $value) {
+    return $this->decoratedFormState->addBuildInfo($property, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getUserInput() {
+    return $this->decoratedFormState->getUserInput();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUserInput(array $user_input) {
+    return $this->decoratedFormState->setUserInput($user_input);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getValues() {
+    return $this->decoratedFormState->getValues();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setResponse(Response $response) {
+    return $this->decoratedFormState->setResponse($response);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getResponse() {
+    return $this->decoratedFormState->getResponse();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRedirect($route_name, array $route_parameters = [], array $options = []) {
+    return $this->decoratedFormState->setRedirect($route_name, $route_parameters, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRedirectUrl(Url $url) {
+    return $this->decoratedFormState->setRedirectUrl($url);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRedirect() {
+    return $this->decoratedFormState->getRedirect();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function hasAnyErrors() {
+    return FormState::hasAnyErrors();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setErrorByName($name, $message = '') {
+    return $this->decoratedFormState->setErrorByName($name, $message);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setError(array &$element, $message = '') {
+    return $this->decoratedFormState->setError($element, $message);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearErrors() {
+    return $this->decoratedFormState->clearErrors();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getError(array $element) {
+    return $this->decoratedFormState->getError($element);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getErrors() {
+    return $this->decoratedFormState->getErrors();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRebuild($rebuild = TRUE) {
+    return $this->decoratedFormState->setRebuild($rebuild);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isRebuilding() {
+    return $this->decoratedFormState->isRebuilding();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareCallback($callback) {
+    return $this->decoratedFormState->prepareCallback($callback);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setFormObject(FormInterface $form_object) {
+    return $this->decoratedFormState->setFormObject($form_object);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormObject() {
+    return $this->decoratedFormState->getFormObject();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCleanValueKeys() {
+    return $this->decoratedFormState->getCleanValueKeys();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCleanValueKeys(array $cleanValueKeys) {
+    return $this->decoratedFormState->setCleanValueKeys($cleanValueKeys);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCleanValueKey($cleanValueKey) {
+    return $this->decoratedFormState->addCleanValueKey($cleanValueKey);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function cleanValues() {
+    return $this->decoratedFormState->cleanValues();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormStateInterface.php b/core/lib/Drupal/Core/Form/FormStateInterface.php
index fd46f70..a396440 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 subform.
+   *
+   * @param mixed[] $subform
+   *   The subform for which to get the form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   */
+  public function getFormStateForSubform(array $subform);
+
 }
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..746dfc7
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/SubFormState.php
@@ -0,0 +1,117 @@
+<?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 subform.
+   *
+   * @var mixed[]
+   */
+  protected $subform = [];
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param mixed[] $subform
+   *   The subform for which to create a form state.
+   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
+   *   The parent form state.
+   */
+  protected function __construct(array &$subform, FormStateInterface $parent_form_state) {
+    $this->decoratedFormState = $parent_form_state;
+    $this->subform = $subform;
+  }
+
+  /**
+   * Creates a new instance for a subform.
+   *
+   * @param mixed[] $subform
+   *   The subform for which to create a form state.
+   * @param \Drupal\Core\Form\FormStateInterface $parent_form_state
+   *   The parent form state.
+   *
+   * @return static
+   */
+  public static function createForSubform(array &$subform, FormStateInterface $parent_form_state) {
+    return new static($subform, $parent_form_state);
+  }
+
+  /**
+   * Gets a subform property value.
+   *
+   * @param string $property
+   *   The property name.
+   *
+   * @return mixed
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when the requested property does not exist.
+   */
+  protected function getSubFormProperty($property) {
+    if (!array_key_exists($property, $this->subform)) {
+      throw new \InvalidArgumentException(sprintf('The subform must contain the %s property. Try calling this method from a #process callback instead.', $property));
+    }
+
+    return $this->subform[$property];
+  }
+
+  /**
+   * Gets the subform's parents.
+   *
+   * @return string[]
+   *   The parent keys (#array_parents).
+   */
+  protected function getSubFormParents() {
+    return $this->getSubFormProperty('#array_parents');
+  }
+
+  /**
+   * Gets the subform state's parents.
+   *
+   * @return string[]
+   *   The parent keys (#parents).
+   */
+  protected function getSubFormStateParents() {
+    return $this->getSubFormProperty('#parents');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getValues() {
+    $exists = NULL;
+    $values = &NestedArray::getValue(parent::getValues(), $this->getSubFormStateParents(), $exists);
+    if (!$exists) {
+      $values = [];
+    }
+    elseif (!is_array($values)) {
+      throw new \UnexpectedValueException('The form state values do not belong to the subform.');
+    }
+
+
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormStateForSubform(array $element) {
+    return static::createForSubform($element, $this);
+  }
+
+}
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..0c3287f 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -125,7 +125,9 @@ public function form(array $form, FormStateInterface $form_state) {
     $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
 
     $form['#tree'] = TRUE;
-    $form['settings'] = $entity->getPlugin()->buildConfigurationForm(array(), $form_state);
+    $form['settings'] = [];
+    $subform_state = $form_state->getFormStateForSubform($form['settings']);
+    $form['settings'] = $entity->getPlugin()->buildConfigurationForm($form['settings'], $subform_state);
     $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
 
     // If creating a new block, calculate a safe default machine name.
@@ -285,11 +287,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['settings'], $form_state->getFormStateForSubform($form['settings']));
     $this->validateVisibility($form, $form_state);
   }
 
@@ -313,11 +311,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['visibility'][$condition_id], $form_state->getFormStateForSubform($form['visibility'][$condition_id]));
     }
   }
 
@@ -330,29 +324,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['settings'], $form_state->getFormStateForSubform($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['visibility'][$condition_id], $form_state->getFormStateForSubform($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..538d54f 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;
 
@@ -79,7 +79,9 @@ public function buildForm(array $form, FormStateInterface $form_state, ImageStyl
       '#value' => $this->imageEffect->getPluginId(),
     );
 
-    $form['data'] = $this->imageEffect->buildConfigurationForm(array(), $form_state);
+    $form['data'] = [];
+    $subform_state = $form_state->getFormStateForSubform($form['data']);
+    $form['data'] = $this->imageEffect->buildConfigurationForm($form['data'], $subform_state);
     $form['data']['#tree'] = TRUE;
 
     // Check the URL for a weight, then the image effect, otherwise use default.
@@ -108,10 +110,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->getFormStateForSubform($form['data']));
   }
 
   /**
@@ -122,10 +121,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->getFormStateForSubform($form['data']));
 
     $this->imageEffect->setWeight($form_state->getValue('weight'));
     if (!$this->imageEffect->getUuid()) {
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
index 24904e3..fd98431 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php
@@ -143,75 +143,6 @@ public function testFormErrorsDuringSubmission() {
   }
 
   /**
-   * Tests that setting the value for an element adds to the values.
-   *
-   * @covers ::setValueForElement
-   */
-  public function testSetValueForElement() {
-    $element = array(
-      '#parents' => array(
-        'foo',
-        'bar',
-      ),
-    );
-    $value = $this->randomMachineName();
-
-    $form_state = new FormState();
-    $form_state->setValueForElement($element, $value);
-    $expected = array(
-      'foo' => array(
-        'bar' => $value,
-      ),
-    );
-    $this->assertSame($expected, $form_state->getValues());
-  }
-
-  /**
-   * @covers ::getValue
-   *
-   * @dataProvider providerTestGetValue
-   */
-  public function testGetValue($key, $expected, $default = NULL) {
-    $form_state = (new FormState())->setValues([
-      'foo' => 'one',
-      'bar' => array(
-        'baz' => 'two',
-      ),
-    ]);
-    $this->assertSame($expected, $form_state->getValue($key, $default));
-  }
-
-  public function providerTestGetValue() {
-    $data = array();
-    $data[] = array(
-      'foo', 'one',
-    );
-    $data[] = array(
-      array('bar', 'baz'), 'two',
-    );
-    $data[] = array(
-      array('foo', 'bar', 'baz'), NULL,
-    );
-    $data[] = array(
-      'baz', 'baz', 'baz',
-    );
-    return $data;
-  }
-
-  /**
-   * @covers ::setValue
-   *
-   * @dataProvider providerTestSetValue
-   */
-  public function testSetValue($key, $value, $expected) {
-    $form_state = (new FormState())->setValues([
-      'bar' => 'wrong',
-    ]);
-    $form_state->setValue($key, $value);
-    $this->assertSame($expected, $form_state->getValues());
-  }
-
-  /**
    * @covers ::prepareCallback
    */
   public function testPrepareCallbackValidMethod() {
@@ -243,102 +174,6 @@ public function testPrepareCallbackArray() {
     $this->assertEquals($callback, $processed_callback);
   }
 
-  public function providerTestSetValue() {
-    $data = array();
-    $data[] = array(
-      'foo', 'one', array('bar' => 'wrong', 'foo' => 'one'),
-    );
-    $data[] = array(
-      array('bar', 'baz'), 'two', array('bar' => array('baz' => 'two')),
-    );
-    $data[] = array(
-      array('foo', 'bar', 'baz'), NULL, array('bar' => 'wrong', 'foo' => array('bar' => array('baz' => NULL))),
-    );
-    return $data;
-  }
-
-  /**
-   * @covers ::hasValue
-   *
-   * @dataProvider providerTestHasValue
-   */
-  public function testHasValue($key, $expected) {
-    $form_state = (new FormState())->setValues([
-      'foo' => 'one',
-      'bar' => array(
-        'baz' => 'two',
-      ),
-      'true' => TRUE,
-      'false' => FALSE,
-      'null' => NULL,
-    ]);
-    $this->assertSame($expected, $form_state->hasValue($key));
-  }
-
-  public function providerTestHasValue() {
-    $data = array();
-    $data[] = array(
-      'foo', TRUE,
-    );
-    $data[] = array(
-      array('bar', 'baz'), TRUE,
-    );
-    $data[] = array(
-      array('foo', 'bar', 'baz'), FALSE,
-    );
-    $data[] = array(
-      'true', TRUE,
-    );
-    $data[] = array(
-      'false', TRUE,
-    );
-    $data[] = array(
-      'null', FALSE,
-    );
-    return $data;
-  }
-
-  /**
-   * @covers ::isValueEmpty
-   *
-   * @dataProvider providerTestIsValueEmpty
-   */
-  public function testIsValueEmpty($key, $expected) {
-    $form_state = (new FormState())->setValues([
-      'foo' => 'one',
-      'bar' => array(
-        'baz' => 'two',
-      ),
-      'true' => TRUE,
-      'false' => FALSE,
-      'null' => NULL,
-    ]);
-    $this->assertSame($expected, $form_state->isValueEmpty($key));
-  }
-
-  public function providerTestIsValueEmpty() {
-    $data = array();
-    $data[] = array(
-      'foo', FALSE,
-    );
-    $data[] = array(
-      array('bar', 'baz'), FALSE,
-    );
-    $data[] = array(
-      array('foo', 'bar', 'baz'), TRUE,
-    );
-    $data[] = array(
-      'true', FALSE,
-    );
-    $data[] = array(
-      'false', TRUE,
-    );
-    $data[] = array(
-      'null', TRUE,
-    );
-    return $data;
-  }
-
   /**
    * @covers ::loadInclude
    */
@@ -583,6 +418,23 @@ public function testCleanValues($form_state) {
     $form_state->setValue('value_to_keep', 'magic_ponies');
     $this->assertSame($form_state->cleanValues()->getValues(), ['value_to_keep' => 'magic_ponies']);
   }
+
+  /**
+   * @covers ::getFormStateForSubform
+   */
+  public function testGetFormStateForSubform() {
+    $form_state = new FormState();
+    $element = [
+      '#parents' => ['foo'],
+      '#array_parents' => ['foo'],
+      'bar' => [],
+    ];
+
+    $sub_form_state = $form_state->getFormStateForSubform($element);
+    $this->assertNotSame($form_state, $sub_form_state);
+    $this->assertInstanceOf(FormStateInterface::class, $sub_form_state);
+  }
+
 }
 
 /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php
new file mode 100644
index 0000000..822bafb
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormStateValuesTraitTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Form\FormStateValuesTrait;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormStateValuesTrait
+ *
+ * @group Form
+ */
+class FormStateValuesTraitTest extends UnitTestCase {
+
+  /**
+   * Tests that setting the value for an element adds to the values.
+   *
+   * @covers ::setValueForElement
+   */
+  public function testSetValueForElement() {
+    $element = array(
+      '#parents' => array(
+        'foo',
+        'bar',
+      ),
+    );
+    $value = $this->randomMachineName();
+
+    $form_state = new FormStateValuesTraitStub();
+    $form_state->setValueForElement($element, $value);
+    $expected = array(
+      'foo' => array(
+        'bar' => $value,
+      ),
+    );
+    $this->assertSame($expected, $form_state->getValues());
+  }
+
+  /**
+   * @covers ::getValue
+   *
+   * @dataProvider providerTestGetValue
+   */
+  public function testGetValue($key, $expected, $default = NULL) {
+    $form_state = (new FormStateValuesTraitStub())->setValues([
+      'foo' => 'one',
+      'bar' => array(
+        'baz' => 'two',
+      ),
+    ]);
+    $this->assertSame($expected, $form_state->getValue($key, $default));
+  }
+
+  public function providerTestGetValue() {
+    $data = array();
+    $data[] = array(
+      'foo', 'one',
+    );
+    $data[] = array(
+      array('bar', 'baz'), 'two',
+    );
+    $data[] = array(
+      array('foo', 'bar', 'baz'), NULL,
+    );
+    $data[] = array(
+      'baz', 'baz', 'baz',
+    );
+    $data[] = array(
+      NULL,
+      [
+        'foo' => 'one',
+        'bar' => [
+          'baz' => 'two',
+        ],
+      ],
+    );
+    return $data;
+  }
+
+  /**
+   * @covers ::getValue
+   */
+  public function testGetValueModifyReturn() {
+    $initial_values = $values = [
+      'foo' => 'one',
+      'bar' => [
+        'baz' => 'two',
+      ],
+    ];
+    $form_state = (new FormStateValuesTraitStub())->setValues($values);
+
+    $value = &$form_state->getValue(NULL);
+    $this->assertSame($initial_values, $value);
+    $value = ['bing' => 'bang'];
+    $this->assertSame(['bing' => 'bang'], $form_state->getValues());
+    $this->assertSame('bang', $form_state->getValue('bing'));
+    $this->assertSame(['bing' => 'bang'], $form_state->getValue(NULL));
+  }
+
+  /**
+   * @covers ::setValue
+   *
+   * @dataProvider providerTestSetValue
+   */
+  public function testSetValue($key, $value, $expected) {
+    $form_state = (new FormStateValuesTraitStub())->setValues([
+      'bar' => 'wrong',
+    ]);
+    $form_state->setValue($key, $value);
+    $this->assertSame($expected, $form_state->getValues());
+  }
+
+  public function providerTestSetValue() {
+    $data = array();
+    $data[] = array(
+      'foo', 'one', array('bar' => 'wrong', 'foo' => 'one'),
+    );
+    $data[] = array(
+      array('bar', 'baz'), 'two', array('bar' => array('baz' => 'two')),
+    );
+    $data[] = array(
+      array('foo', 'bar', 'baz'), NULL, array('bar' => 'wrong', 'foo' => array('bar' => array('baz' => NULL))),
+    );
+    return $data;
+  }
+
+  /**
+   * @covers ::hasValue
+   *
+   * @dataProvider providerTestHasValue
+   */
+  public function testHasValue($key, $expected) {
+    $form_state = (new FormStateValuesTraitStub())->setValues([
+      'foo' => 'one',
+      'bar' => array(
+        'baz' => 'two',
+      ),
+      'true' => TRUE,
+      'false' => FALSE,
+      'null' => NULL,
+    ]);
+    $this->assertSame($expected, $form_state->hasValue($key));
+  }
+
+  public function providerTestHasValue() {
+    $data = array();
+    $data[] = array(
+      'foo', TRUE,
+    );
+    $data[] = array(
+      array('bar', 'baz'), TRUE,
+    );
+    $data[] = array(
+      array('foo', 'bar', 'baz'), FALSE,
+    );
+    $data[] = array(
+      'true', TRUE,
+    );
+    $data[] = array(
+      'false', TRUE,
+    );
+    $data[] = array(
+      'null', FALSE,
+    );
+    return $data;
+  }
+
+  /**
+   * @covers ::isValueEmpty
+   *
+   * @dataProvider providerTestIsValueEmpty
+   */
+  public function testIsValueEmpty($key, $expected) {
+    $form_state = (new FormStateValuesTraitStub())->setValues([
+      'foo' => 'one',
+      'bar' => array(
+        'baz' => 'two',
+      ),
+      'true' => TRUE,
+      'false' => FALSE,
+      'null' => NULL,
+    ]);
+    $this->assertSame($expected, $form_state->isValueEmpty($key));
+  }
+
+  public function providerTestIsValueEmpty() {
+    $data = array();
+    $data[] = array(
+      'foo', FALSE,
+    );
+    $data[] = array(
+      array('bar', 'baz'), FALSE,
+    );
+    $data[] = array(
+      array('foo', 'bar', 'baz'), TRUE,
+    );
+    $data[] = array(
+      'true', FALSE,
+    );
+    $data[] = array(
+      'false', TRUE,
+    );
+    $data[] = array(
+      'null', TRUE,
+    );
+    return $data;
+  }
+
+}
+
+class FormStateValuesTraitStub {
+  use FormStateValuesTrait;
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/SubFormStateTest.php b/core/tests/Drupal/Tests/Core/Form/SubFormStateTest.php
new file mode 100644
index 0000000..2836324
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/SubFormStateTest.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\SubFormStateTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\SubFormState
+ *
+ * @group Form
+ */
+class SubFormStateTest extends UnitTestCase {
+
+  /**
+   * Test fixture.
+   *
+   * @var array
+   */
+  protected $initialValues = [
+    'foo' => 'bar',
+    'dog' => [
+      'breed' => 'Pit bull',
+      'name' => 'Dodger',
+    ],
+  ];
+  protected $element = [
+    'foo' => [
+      '#parents' => ['foo'],
+      '#array_parents' => ['foo'],
+    ],
+    'dog' => [
+      '#parents' => ['dog'],
+      '#array_parents' => ['dog'],
+      'breed' => [
+        '#parents' => ['dog', 'breed'],
+        '#array_parents' => ['dog', 'breed'],
+      ],
+      'name' => [
+        '#parents' => ['dog', 'name'],
+        '#array_parents' => ['dog', 'name'],
+      ],
+    ],
+  ];
+
+  /**
+   * @covers ::getFormStateForSubform
+   */
+  public function testGetFormStateForSubform() {
+    $form_state = new FormState();
+    $element = [
+      '#parents' => ['foo'],
+      '#array_parents' => ['foo'],
+      'bar' => [],
+    ];
+
+    $sub_form_state = $form_state->getFormStateForSubform($element);
+    $this->assertNotSame($form_state, $sub_form_state);
+    $this->assertInstanceOf(FormStateInterface::class, $sub_form_state);
+  }
+
+  /**
+   * @covers ::getValues
+   *
+   * @dataProvider providerTestGetValues
+   *
+   * @param string[] $parents
+   * @param string $expected
+   */
+  public function testGetValues(array $parents, $expected) {
+    $form_state = new FormState();
+    $form_state->setValues($this->initialValues);
+
+    $element = NestedArray::getValue($this->element, $parents);
+    $sub_form_state = $form_state->getFormStateForSubform($element);
+    $sub_values = $sub_form_state->getValues();
+    $this->assertSame($expected, $sub_values);
+  }
+
+  /**
+   * Provides data to self::testGetValues().
+   */
+  public function providerTestGetValues() {
+    return [
+      [['dog'], $this->initialValues['dog']],
+    ];
+  }
+
+  /**
+   * @covers ::getValues
+   *
+   * @dataProvider providerTestGetValuesBroken
+   *
+   * @expectedException \UnexpectedValueException
+   *
+   * @param string[] $parents
+   * @param string $expected
+   */
+  public function testGetValuesBroken(array $parents, $expected) {
+    $this->testGetValues($parents, $expected);
+  }
+
+  /**
+   * Provides data to self::testGetValuesBroken().
+   */
+  public function providerTestGetValuesBroken() {
+    return [
+      [['foo'], $this->initialValues['foo']],
+      [['dog', 'name'], 'Dodger'],
+    ];
+  }
+
+  /**
+   * @covers ::getValue
+   *
+   * @dataProvider providerTestGetValue
+   */
+  public function testGetValue($parents, $key, $expected, $default = NULL) {
+    $form_state = new FormState();
+    $form_state->setValues($this->initialValues);
+
+    $element = NestedArray::getValue($this->element, $parents);
+    $sub_form_state = $form_state->getFormStateForSubform($element);
+    $sub_values = $sub_form_state->getValue($key, $default);
+    $this->assertSame($expected, $sub_values);
+  }
+
+  /**
+   * Provides data to self::testGetValue().
+   */
+  public function providerTestGetValue() {
+    return [
+      [['dog'], 'name', 'Dodger'],
+    ];
+  }
+
+  /**
+   * @covers ::getValue
+   *
+   * @dataProvider providerTestGetValueBroken
+   *
+   * @expectedException \UnexpectedValueException
+   */
+  public function testGetValueBroken(array $parents, $key, $expected, $default = NULL) {
+    $this->testGetValue($parents, $key, $expected, $default);
+  }
+
+  /**
+   * Provides data to self::testGetValueBroken().
+   */
+  public function providerTestGetValueBroken() {
+    return [
+      [['dog', 'name'], NULL, 'Dodger'],
+    ];
+  }
+
+  /**
+   * @covers ::setValues
+   *
+   * @dataProvider providerTestSetValues
+   */
+  public function testSetValues($parents, $new_values, $expected) {
+    $form_state = new FormState();
+    $form_state->setValues($this->initialValues);
+
+    $element = NestedArray::getValue($this->element, $parents);
+    $sub_form_state = $form_state->getFormStateForSubform($element);
+    $sub_form_state->setValues($new_values);
+    $this->assertSame($expected, $form_state->getValues());
+  }
+
+  /**
+   * Provides data to self::testSetValues().
+   */
+  public function providerTestSetValues() {
+    return [
+      [['dog'], [], [
+        'foo' => 'bar',
+        'dog' => [],
+      ]],
+    ];
+  }
+
+  /**
+   * @covers ::setValues
+   *
+   * @dataProvider providerTestSetValuesBroken
+   *
+   * @expectedException \UnexpectedValueException
+   */
+  public function testSetValuesBroken($parents, $new_values, $expected) {
+    $this->testSetValues($parents, $new_values, $expected);
+  }
+
+  /**
+   * Provides data to self::testSetValuesBroken().
+   */
+  public function providerTestSetValuesBroken() {
+    $data = [];
+    $data['exist'] = [
+      ['foo'],
+      [],
+      [
+        'foo' => [],
+        'dog' => $this->initialValues['dog'],
+      ],
+    ];
+    return $data;
+  }
+
+}
