How do I store node form data between form steps using ajax? I want to turn a node form into a multi-step form using ajax. I've added ajax-enabled submit buttons and some logic in a hook_form_alter (see below) to handle inc- and decrementing steps, and showing and hiding form items based on the current step. The stepping back and forth works great, in terms of rendering the correct elements.

The problem is, if I enter some data in one step, go forward or backward, and then return to the original step, the data I entered is gone from the form. If I check the value of the item for which I entered data using FormStateInterface::getValue(), the data is there when my alter hook runs when leaving the original step, but gone on subsequent next/back transitions. This problem also does not apply to data that is already in the form (e.g. when editing an existing node).

Clearly, I need to store the incoming values somewhere, but where? And then, how do I restore them to the form elements on the appropriate step?

My code (minimal example):

&lt;?php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_node_ajax_test_form_alter(&amp;$form, $form_state) {
  // Define steps. Each child array is a step, and each string in the
  // a child array is a field that should be present on that step.
  $steps = [
    ['title'],
    ['body'],
  ];
  // Add wrapper.
  $form['#prefix'] = '&lt;div id="ajax-form-wrapper"&gt;';
  $form['#suffix'] = '&lt;/div&gt;';
  // If the step is not set, this is step 0.
  if (!$form_state-&gt;has('step')) {
    $form_state-&gt;set('step', 0);
  }
  // Get the current step.
  $step = $form_state-&gt;get('step');
  // If we have a triggering element, increment or decrement step as
  // appropriate.
  if ($trigger = $form_state-&gt;getTriggeringElement()) {
    switch ($trigger['#name']) {
      case 'next':
        $form_state-&gt;set('step', ++$step);
        break;

      case 'back':
        $form_state-&gt;set('step', --$step);
        break;
    }
  }
  // Show elements in the current step, hide others. Always show the
  // `actions` element, and all `form_*` elements.      
  foreach (Element::children($form) as $element) {
    $form[$element]['#access'] = (
      in_array($element, $steps[$step])
      || $element == 'actions'
      || strpos($element, 'form') === 0
    );
  }
  // Show the back button if this is not the first step.
  $form['actions']['back'] = $step &gt; 0 ? ajax_button('back') : NULL;
  // Show the next button if this is not the last step.
  $is_last_step = $step == count($steps) - 1;
  $form['actions']['next'] = !$is_last_step ? ajax_button('next') : NULL;
  // Show the submit button if this is the last step.
  $form['actions']['submit']['#access'] = $is_last_step;
}

/**
 * Return a render array for an ajax step button.
 */
function ajax_button($name) {
  return [
    '#type' =&gt; 'button',
    '#value' =&gt; $name,
    '#name' =&gt; $name,
    '#ajax' =&gt; [
      'wrapper' =&gt; 'ajax-form-wrapper',
    ],
  ];
}
This code is quite impressive. I did not think it was possible to build a multi-step entity form with a few lines in a form alter hook. To keep it this simple you can try to use the entity object to store the values, because the form object is serialized and cached between the steps and a method to build such an entity from the submitted values is part of the entity form class:

  if ($trigger = $form_state-&gt;getTriggeringElement()) {
    $form_object = $form_state-&gt;getFormObject();
    $new_entity = $form_object-&gt;buildEntity($form, $form_state);
    $form_object-&gt;setEntity($new_entity);