{"id":"l-rn-3-4","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Build Trailhead’s add-hike form so it works with a phone keyboard, moving between fields, staying visible above the keyboard, saving on the first tap, and validating with clear per-field errors.","html":"<p>Forms are where mobile apps feel most different from the web. The keyboard covers half the screen, there’s no Tab key, the first tap on a button sometimes only closes the keyboard, and typing on glass is slow enough that every unnecessary error message feels personal. Trailhead’s “Log a hike” form has five fields: name, date, distance, duration and notes. Here’s how to make it pleasant.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">A draft of raw strings</h2>\n<p>Keep everything the user typed in one object of <strong>strings</strong>, exactly as typed. Parse when you need numbers, as you did with <code dir=\"ltr\">parseDistanceKm</code> in section 1:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/app/new-hike.tsx</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useRef, useState, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ComponentRef } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { TextInput } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">duration</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">notes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Today's date in the phone's time zone. toISOString() would give the UTC date,</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// which is already tomorrow (or still yesterday) for part of the day in most places.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> localToday</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> now</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Date</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> pad</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">padStart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getFullYear</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">pad</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getMonth</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">pad</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getDate</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">())</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> emptyDraft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, date: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">localToday</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), distance: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, duration: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, notes: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> NewHike</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(emptyDraft);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">submitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setSubmitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">touched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Partial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>>>({});</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> distanceRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ComponentRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TextInput>>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> durationRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ComponentRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TextInput>>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> set</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">field</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d, [field]: text }));</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …errors, save and JSX below</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The <code dir=\"ltr\">set('distance')</code> helper gives each input an <code dir=\"ltr\">onChangeText</code> without five near-identical functions. Storing strings means “8,” stays “8,” while the user types, instead of collapsing to <code dir=\"ltr\">8</code> or <code dir=\"ltr\">NaN</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Moving between fields</h2>\n<p>There’s no Tab key, so the return key does the job. Label it with <code dir=\"ltr\">returnKeyType=&quot;next&quot;</code>, and when it’s pressed, focus the next input through a ref:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">draft.name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onChangeText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'name'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  placeholder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Ridge Loop\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  autoCapitalize</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"words\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  returnKeyType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"next\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  submitBehavior</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onSubmitEditing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> distanceRef.current?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onBlur</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t, name: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }))</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">/></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ref</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">distanceRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">draft.distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onChangeText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  keyboardType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"decimal-pad\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  returnKeyType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"next\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onSubmitEditing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> durationRef.current?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onBlur</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t, distance: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }))</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">/></span></span></code></pre></div>\n<p><code dir=\"ltr\">submitBehavior=&quot;submit&quot;</code> keeps the keyboard open while focus moves, so it doesn’t flicker closed and open again. On the last field, use <code dir=\"ltr\">returnKeyType=&quot;done&quot;</code> and let it submit the form. One honest caveat: numeric keypads on iOS have no return key at all, so users tap the next field or a toolbar button instead. That’s normal, and another reason not to rely on the keyboard alone.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Keeping fields above the keyboard</h2>\n<p>When the keyboard opens, it sits on top of your app. A field near the bottom (the notes) disappears behind it. The standard fix wraps the form in <code dir=\"ltr\">KeyboardAvoidingView</code> and puts a <code dir=\"ltr\">ScrollView</code> inside:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { KeyboardAvoidingView, Platform, ScrollView } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">KeyboardAvoidingView</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> behavior</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Platform.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">OS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'ios'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'padding'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ScrollView</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    contentContainerStyle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    keyboardShouldPersistTaps</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"handled\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    keyboardDismissMode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"on-drag\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ></span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    {</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* fields and the Save button */</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ScrollView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">KeyboardAvoidingView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>On iOS, <code dir=\"ltr\">padding</code> adds bottom padding equal to the keyboard height, so the ScrollView shrinks and the focused field can scroll into view. On Android the window usually resizes for the keyboard already, so <code dir=\"ltr\">undefined</code> is the recommended value. <code dir=\"ltr\">keyboardDismissMode=&quot;on-drag&quot;</code> closes the keyboard when the user scrolls, which feels natural.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>The Save button that needs two taps</span></p>\n<p>By default, a ScrollView treats a tap outside the focused input as “dismiss the keyboard” and nothing else. The user taps Save, the keyboard closes, and nothing saves; they tap again. <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code> lets taps on buttons and other touchables go through while still dismissing the keyboard for taps on empty space. Put it on every ScrollView or FlatList that contains inputs.</p>\n</aside>\n<p>For long forms with many fields, Expo’s docs recommend <code dir=\"ltr\">react-native-keyboard-controller</code>, whose <code dir=\"ltr\">KeyboardAwareScrollView</code> scrolls the focused field into view automatically; the same library’s <code dir=\"ltr\">KeyboardToolbar</code> adds previous, next and done buttons above the keyboard. It needs a development build, which Trailhead gets in section 4.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">A word about dates</h2>\n<p>Trailhead’s draft keeps the date as a <code dir=\"ltr\">YYYY-MM-DD</code> string, defaulted to today, because most hikes are logged the same day. Typing dates on a phone keyboard is miserable, though, so production apps show the platform’s own date picker: a wheel or calendar on iOS, a dialog on Android. The community package <code dir=\"ltr\">@react-native-community/datetimepicker</code> wraps both, and it’s a good example of a component whose behaviour differs enough by platform that you’ll want the per-platform files from section 2.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Validation that doesn’t nag</h2>\n<p>Keep the rules in a pure function: draft in, errors out. An empty object means valid.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/lib/validateHike.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Partial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d.name.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()) errors.name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Give the hike a name.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(d.distance) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) errors.distance </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Enter a distance in km, like 8.4.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> errors;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Then decide <strong>when</strong> to show each message. Showing errors on the first keystroke punishes people for not having finished typing. Waiting for an alert after Save hides the form behind a list they must memorize. The pattern that works: show a field’s error once the user has left it (<code dir=\"ltr\">touched</code>) or pressed Save (<code dir=\"ltr\">submitted</code>), next to the field, and keep it live from then on so it disappears the moment they fix it.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> show</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">field</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (submitted </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> touched[field]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> errors[field];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setSubmitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(errors).</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  dispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ type: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'added'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, hike: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft) });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  router.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">back</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// under the distance input:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">show</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ color: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#b42318'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">errors.distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Two accessibility touches cost a line each: give every input an <code dir=\"ltr\">accessibilityLabel</code> that matches its visible label, and keep error text right below its field so screen readers reach it next.</p>\n<p><code dir=\"ltr\">toHike</code> converts the valid draft into a real <code dir=\"ltr\">Hike</code> (parsing numbers, generating an id). After saving, <code dir=\"ltr\">router.back()</code> returns to the log, where the new hike is already at the top because the reducer put it there.</p>\n<p>The form currently opens as a normal pushed screen. Next you’ll present it as a modal, protect screens behind sign-in, and let links open Trailhead on a specific hike.</p>\n","toc":[{"id":"sec-1","text":"A draft of raw strings","level":2},{"id":"sec-2","text":"Moving between fields","level":2},{"id":"sec-3","text":"Keeping fields above the keyboard","level":2},{"id":"sec-4","text":"A word about dates","level":2},{"id":"sec-5","text":"Validation that doesn’t nag","level":2}],"takeaways":["Keep the form draft as raw strings in one state object, and parse and validate only when you need the result.","Chain fields with <code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> and refs, so the return key moves to the next input instead of closing the keyboard.","Wrap the form in <code dir=\"ltr\">KeyboardAvoidingView</code> with <code dir=\"ltr\">behavior=&quot;padding&quot;</code> on iOS, and give the inner ScrollView <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code>.","Validate in a pure function that returns an errors object, and show each error next to its field after the first submit or blur."],"further":[{"title":"Keyboard handling","url":"https://docs.expo.dev/guides/keyboard-handling/"},{"title":"KeyboardAvoidingView","url":"https://reactnative.dev/docs/keyboardavoidingview"},{"title":"ScrollView keyboardShouldPersistTaps","url":"https://reactnative.dev/docs/scrollview#keyboardshouldpersisttaps"}],"quiz":[{"q":"<p>Users report they have to tap Save twice on Trailhead’s form while the keyboard is open. The first tap only closes the keyboard. What fixes it?</p>","options":[{"text":"Call <code dir=\"ltr\">Keyboard.dismiss()</code> at the start of the save handler.","why":"The save handler never ran on the first tap, so code inside it can’t help."},{"text":"Set <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code> on the ScrollView that contains the form.","why":"Correct. By default, a tap outside the focused input while the keyboard is up only dismisses the keyboard; <code dir=\"ltr\">handled</code> lets taps on buttons go through."},{"text":"Move the Save button above the inputs.","why":"The tap is swallowed wherever the button is, as long as it’s inside that ScrollView."}],"answer":1},{"q":"<p>On iOS, the notes field at the bottom of the form is hidden behind the keyboard when focused. Which setup is the usual fix?</p>","options":[{"text":"A <code dir=\"ltr\">KeyboardAvoidingView</code> with <code dir=\"ltr\">behavior=&quot;padding&quot;</code> and <code dir=\"ltr\">flex: 1</code> around the form’s ScrollView.","why":"Correct. The view adds bottom padding equal to the keyboard’s height, so the ScrollView shrinks and can scroll the field into view."},{"text":"<code dir=\"ltr\">position: 'absolute'</code> on the notes field with a high <code dir=\"ltr\">zIndex</code>.","why":"The keyboard is a system window above your app; no zIndex puts your view over it."},{"text":"Set <code dir=\"ltr\">autoFocus</code> on the notes field.","why":"That opens the keyboard on mount, which makes the overlap happen sooner, not go away."}],"answer":0},{"q":"<p>When should Trailhead show the error “Distance must be a number” under the distance field?</p>","options":[{"text":"On every keystroke from the first character.","why":"Typing “8,” on the way to “8,4” is momentarily invalid. Errors while the user is still typing are noise."},{"text":"Only in an alert after Save, listing every error at once.","why":"An alert hides the form, and the user has to remember the list after closing it."},{"text":"After the user leaves the field or presses Save, next to the field, and updating live once it’s showing.","why":"Correct. Wait until the user is done with a field, then keep the message in sync as they fix it."}],"answer":2},{"q":"<p>How do you make the return key on the name field move focus to the distance field?</p>","options":[{"text":"<code dir=\"ltr\">tabIndex={2}</code> on the distance field.","why":"That’s a web attribute. Phone keyboards have no tab order to set."},{"text":"<code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> on the name field and <code dir=\"ltr\">autoFocus</code> on the distance field.","why":"<code dir=\"ltr\">autoFocus</code> acts once on mount, not when the name field’s return key is pressed."},{"text":"Nothing; mobile keyboards move to the next field automatically.","why":"They don’t. Without handling it, return closes the keyboard on a single-line input."},{"text":"<code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> plus <code dir=\"ltr\">onSubmitEditing={() =&gt; distanceRef.current?.focus()}</code>, with a ref on the distance field.","why":"Correct. The key label says Next, and the handler moves focus."}],"answer":3}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Return an object with one message per invalid field. {} means the draft is valid.\n// `today` is the current date as 'YYYY-MM-DD', passed in so the function stays pure.\nfunction validateHikeForm(draft, today) {\n  const errors = {};\n  if (draft.name === '') errors.name = 'Give the hike a name.';\n  return errors;\n}\n\nconst draft = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' };\nconsole.log(validateHikeForm(draft, '2026-10-05'));\n"},"solution":{"js":"function isRealDate(text) {\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(text)) return false;\n  const [y, m, d] = text.split('-').map(Number);\n  const date = new Date(Date.UTC(y, m - 1, d));\n  return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;\n}\n\nfunction validateHikeForm(draft, today) {\n  const errors = {};\n  const name = draft.name.trim();\n  if (!name) errors.name = 'Give the hike a name.';\n  else if (name.length > 60) errors.name = 'Keep the name under 60 characters.';\n\n  if (!isRealDate(draft.date)) errors.date = 'Use a real date like 2026-10-04.';\n  else if (draft.date > today) errors.date = \"That date hasn't happened yet.\";\n\n  const cleaned = draft.distance.trim().replace(',', '.');\n  const km = Number(cleaned);\n  if (!/^\\d+(\\.\\d+)?$/.test(cleaned) || km <= 0 || km > 200) errors.distance = 'Enter a distance in km, like 8.4.';\n\n  const duration = draft.duration.trim();\n  if (duration && (!/^\\d+$/.test(duration) || Number(duration) < 1 || Number(duration) > 1440)) {\n    errors.duration = 'Enter whole minutes between 1 and 1440.';\n  }\n\n  if (draft.notes.length > 500) errors.notes = 'Notes are limited to 500 characters.';\n  return errors;\n}\n\nconst draft = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' };\nconsole.log(validateHikeForm(draft, '2026-10-05'));\n"},"checks":[{"id":"valid-draft","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return typeof validateHikeForm === 'function' && keys({}) === '' && keys({ duration: '135', notes: 'Windy.' }) === '';"},{"id":"name","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ name: '   ' }) === 'name' && keys({ name: 'x'.repeat(61) }) === 'name';"},{"id":"date","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ date: '04/10/2026' }) === 'date' && keys({ date: '2026-02-30' }) === 'date' && keys({ date: '2026-10-06' }) === 'date' && keys({ date: '2026-10-05' }) === '';"},{"id":"distance","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ distance: '' }) === 'distance' && keys({ distance: 'far' }) === 'distance' && keys({ distance: '0' }) === 'distance' && keys({ distance: '12.5' }) === '';"},{"id":"duration","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ duration: '2h' }) === 'duration' && keys({ duration: '0' }) === 'duration' && keys({ duration: '1441' }) === 'duration';"},{"id":"several","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ name: '', distance: 'x', notes: 'n'.repeat(501) }) === 'distance,name,notes';"}],"title":"Validate the add-hike form","prompt":"<p>Write <code dir=\"ltr\">validateHikeForm(draft, today)</code> for Trailhead’s form. <code dir=\"ltr\">draft</code> holds the raw strings the user typed: <code dir=\"ltr\">name</code>, <code dir=\"ltr\">date</code>, <code dir=\"ltr\">distance</code>, <code dir=\"ltr\">duration</code>, <code dir=\"ltr\">notes</code>. <code dir=\"ltr\">today</code> is today’s date as <code dir=\"ltr\">YYYY-MM-DD</code>. Return an object with a message for <strong>each invalid field</strong> (any non-empty string); return <code dir=\"ltr\">{}</code> when everything is valid.</p>\n<ul>\n<li><strong>name</strong>: required after trimming; at most 60 characters.</li>\n<li><strong>date</strong>: <code dir=\"ltr\">YYYY-MM-DD</code>, a date that exists (<code dir=\"ltr\">2026-02-30</code> doesn’t), and not after <code dir=\"ltr\">today</code>.</li>\n<li><strong>distance</strong>: a number above 0 and at most 200; a comma decimal like <code dir=\"ltr\">8,4</code> is fine.</li>\n<li><strong>duration</strong>: optional; if filled in, whole minutes from 1 to 1440.</li>\n<li><strong>notes</strong>: optional; at most 500 characters.</li>\n</ul>\n","hints":["Build <code dir=\"ltr\">errors</code> field by field with <code dir=\"ltr\">if</code> statements; each failing rule sets <code dir=\"ltr\">errors.field = '…'</code>.","ISO date strings compare correctly as text, so <code dir=\"ltr\">draft.date &gt; today</code> means “in the future”. To reject <code dir=\"ltr\">2026-02-30</code>, build <code dir=\"ltr\">new Date(Date.UTC(y, m - 1, d))</code> and check that the year, month and day come back unchanged.","For the distance, normalize first (<code dir=\"ltr\">trim</code>, comma to dot), then test against <code dir=\"ltr\">/^\\d+(\\.\\d+)?$/</code> before checking the range."],"explanation":"<p>Keeping validation in a pure function that maps a draft to an errors object means the form only needs <code dir=\"ltr\">Object.keys(errors).length === 0</code> to decide whether to save, and each field can look up its own message. Passing <code dir=\"ltr\">today</code> in, rather than calling <code dir=\"ltr\">new Date()</code> inside, keeps the function deterministic, so tests don’t break at midnight. The round-trip check through <code dir=\"ltr\">Date.UTC</code> is a compact way to reject impossible dates, which JavaScript’s <code dir=\"ltr\">Date</code> would otherwise silently roll over into March.</p>\n","checkLabels":{"valid-draft":"A complete, sensible draft has no errors","name":"Blank and overly long names are rejected","date":"Badly formatted, impossible and future dates are rejected","distance":"Missing, non-numeric and zero distances are rejected","duration":"A filled-in duration must be whole minutes from 1 to 1440","several":"Several invalid fields are all reported at once"}},"words":706,"readMinutes":3}