{"id":"l-rn-1-4","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Make Trailhead respond to touch with Pressable and accept typing with a controlled TextInput, including pressed feedback, tap targets, mobile keyboards and parsing what users type.","html":"<p>A web button gets hover styles, focus rings and a pointer cursor for free. On a phone there’s no hover and no cursor: there’s a thumb, which is wide, imprecise and moving. Good touch handling on mobile comes down to three things: a large enough target, feedback the instant the finger lands, and inputs that open the right keyboard.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Pressable: the default for anything tappable</h2>\n<p><code dir=\"ltr\">Pressable</code> wraps any content and reports touches. Its <code dir=\"ltr\">style</code> prop can be a function that receives <code dir=\"ltr\">{ pressed }</code>, so you can dim or tint the button while the finger is down:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/components/PrimaryButton.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\"> { Pressable, Text } </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\"> Props</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">label</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:#8250DF;--shiki-dark:#D2A8FF\">onPress</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:#0550AE;--shiki-dark:#79C0FF\"> void</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</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\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> PrimaryButton</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onPress</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Props</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\"> (</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">onPress</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      accessibilityState</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ disabled }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">      style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">pressed</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:#1F2328;--shiki-dark:#E6EDF3\">        backgroundColor: disabled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '#9bb5a7'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '#2f6f4e'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        opacity: pressed </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.75</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        paddingVertical: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">14</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        paddingHorizontal: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">20</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        borderRadius: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        alignItems: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'center'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><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\">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\">'white'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontWeight: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'600'</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\">label</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Pressable</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">accessibilityRole=&quot;button&quot;</code> makes VoiceOver and TalkBack announce it as a button; the text inside becomes its label. For icon-only buttons, add <code dir=\"ltr\">accessibilityLabel=&quot;Delete hike&quot;</code> because there’s no text to read.</p>\n<p>You’ll also see <code dir=\"ltr\">Button</code> and <code dir=\"ltr\">TouchableOpacity</code> in older code. <code dir=\"ltr\">Button</code> can’t be styled beyond a color, and the <code dir=\"ltr\">Touchable*</code> family is the older API. Reach for <code dir=\"ltr\">Pressable</code> and build your own button component once.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Tap targets and hitSlop</h2>\n<p>Apple’s guideline is a minimum of 44 by 44 points; Material Design says 48 by 48 dp. A 24-point icon is fine visually, but too small to hit while walking down a trail. <code dir=\"ltr\">hitSlop</code> grows the touch area without changing layout:</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\">Pressable</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onPress</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\"> onDelete</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(hike.id)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  hitSlop</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  accessibilityRole</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  accessibilityLabel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Delete ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">hike</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</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\">TrashIcon</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> size</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">24</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\">Pressable</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p><code dir=\"ltr\">Pressable</code> also gives you <code dir=\"ltr\">onLongPress</code> (fired after about half a second by default) and <code dir=\"ltr\">onPressIn</code> / <code dir=\"ltr\">onPressOut</code> for the exact moments the finger lands and lifts. Trailhead uses a long press on a hike card to open a quick-actions sheet, a pattern iOS and Android users already know from their home screens. Use it for shortcuts only: nobody discovers a long press on their own, so every action behind one must also be reachable some other way.</p>\n<p>On Android you can add <code dir=\"ltr\">android_ripple={{ color: '#00000022' }}</code> for the platform’s ripple effect; iOS ignores that prop, so the <code dir=\"ltr\">pressed</code> style still matters there.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">TextInput: controlled, and always a string</h2>\n<p><code dir=\"ltr\">TextInput</code> works like a controlled <code dir=\"ltr\">&lt;input&gt;</code>, with one naming difference. Use <code dir=\"ltr\">onChangeText</code>, which hands you the new string. <code dir=\"ltr\">onChange</code> gives you a native event object instead.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/components/QuickLog.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\"> { useState } </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\"> { Text, TextInput, View } </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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { PrimaryButton } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './PrimaryButton'</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\"> { parseDistanceKm } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '../lib/parseDistance'</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\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> QuickLog</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">onSave</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:#8250DF;--shiki-dark:#D2A8FF\">onSave</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">km</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:#0550AE;--shiki-dark:#79C0FF\"> void</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\">text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setText</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:#0A3069;--shiki-dark:#A5D6FF\">''</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\"> km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><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:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">View</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\">{ gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</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:#1F2328;--shiki-dark:#E6EDF3\">      &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Distance (km)&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</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\">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\">text</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:#1F2328;--shiki-dark:#E6EDF3\">setText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        accessibilityLabel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Distance (km)\"</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\">        placeholder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"8.4\"</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\">\"done\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">        style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ borderWidth: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, borderColor: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#c9d3cc'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, borderRadius: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, fontSize: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">18</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\">PrimaryButton</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Save\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> disabled</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onPress</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\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> onSave</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(km)</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\">View</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Notice what lives in state: the raw <code dir=\"ltr\">text</code>, not a number. If you stored <code dir=\"ltr\">Number(text)</code> instead, typing “8.” would immediately become <code dir=\"ltr\">8</code> and the dot would vanish under the user’s thumb, and clearing the field would give you <code dir=\"ltr\">0</code>. Keep exactly what the user typed, and derive the parsed value during render. The button reads the derived value and stays disabled until it’s valid, which is clearer feedback than an error message that appears mid-typing.</p>\n<p>The props that shape the mobile keyboard are worth knowing by name:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Prop</th>\n<th>What it does</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">keyboardType</code></td>\n<td><code dir=\"ltr\">decimal-pad</code>, <code dir=\"ltr\">number-pad</code>, <code dir=\"ltr\">email-address</code>, <code dir=\"ltr\">phone-pad</code>, <code dir=\"ltr\">url</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">autoCapitalize</code></td>\n<td><code dir=\"ltr\">none</code> for emails and usernames; default capitalizes sentences</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">autoComplete</code> / <code dir=\"ltr\">textContentType</code></td>\n<td>let the OS offer saved emails, passwords and one-time codes</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">returnKeyType</code></td>\n<td>the label on the return key: <code dir=\"ltr\">next</code>, <code dir=\"ltr\">done</code>, <code dir=\"ltr\">search</code></td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">secureTextEntry</code></td>\n<td>hides the characters for passwords</td>\n</tr>\n</tbody>\n</table></div><p>Getting these right is most of what makes a form feel native. An email field that capitalizes the first letter, or a code field that doesn’t offer the one-time code from the SMS that just arrived, tells users the app wasn’t built with care.</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>Trusting the keyboard to validate</span></p>\n<p><code dir=\"ltr\">keyboardType=&quot;decimal-pad&quot;</code> is a hint, not a filter. Users paste text, external keyboards type anything, and on a German phone the decimal key is a comma, so “8,4” arrives as a string <code dir=\"ltr\">Number()</code> can’t read (<code dir=\"ltr\">Number('8,4')</code> is <code dir=\"ltr\">NaN</code>). Parse every input in a small pure function and treat <code dir=\"ltr\">null</code> as “not valid yet”.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Parsing what people type</h2>\n<p>Here is the idea behind <code dir=\"ltr\">parseDistanceKm</code>: normalize first (trim, lower-case, drop a trailing “km”, turn a comma into a dot), then accept only a plain positive number within a sensible range.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"3\" aria-label=\"Run this example\"><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=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><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\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">text</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\"> cleaned</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\">(text).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toLowerCase</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">().</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\s</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">$</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replace</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">','</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </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:#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:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">^</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#116329;--shiki-light-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">\\d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?$</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cleaned)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</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\"> km</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(cleaned);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 200</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> km </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>\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\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> input</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8.4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'8,4'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">' 12 km'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'abc'</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 style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'1.2.3'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">JSON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stringify</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(input), </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'->'</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\">(input));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>Because it’s a plain function with no React Native imports, you can test it in milliseconds, which you’ll do in section 5. Keeping logic like this out of components is a habit that pays for itself on mobile, where running the app to check a case takes much longer than on the web.</p>\n<p>That completes the core toolkit. Section 2 is about making it look right: styling without the cascade, flexbox with different defaults, and screens of every size.</p>\n","toc":[{"id":"sec-1","text":"Pressable: the default for anything tappable","level":2},{"id":"sec-2","text":"Tap targets and hitSlop","level":2},{"id":"sec-3","text":"TextInput: controlled, and always a string","level":2},{"id":"sec-4","text":"Parsing what people type","level":2}],"takeaways":["<code dir=\"ltr\">Pressable</code> is the default for anything tappable; its <code dir=\"ltr\">style</code> can be a function of <code dir=\"ltr\">{ pressed }</code> for instant feedback.","Tap targets should be at least 44 by 44 points; <code dir=\"ltr\">hitSlop</code> enlarges the touch area without changing the layout.","A controlled <code dir=\"ltr\">TextInput</code> uses <code dir=\"ltr\">value</code> plus <code dir=\"ltr\">onChangeText</code>, which receives the string directly.","<code dir=\"ltr\">keyboardType</code> changes the keyboard, not the value: a <code dir=\"ltr\">TextInput</code> always gives you a string, so parse and validate it yourself."],"further":[{"title":"Pressable","url":"https://reactnative.dev/docs/pressable"},{"title":"TextInput","url":"https://reactnative.dev/docs/textinput"},{"title":"Handling Touches","url":"https://reactnative.dev/docs/handling-touches"}],"quiz":[{"q":"<p>You set <code dir=\"ltr\">keyboardType=&quot;decimal-pad&quot;</code> on Trailhead’s distance input. What type is the value in <code dir=\"ltr\">onChangeText</code>?</p>","options":[{"text":"A number, because the keyboard only allows digits.","why":"The keyboard type only changes which keys are shown. The value is always text, and paste can still insert anything."},{"text":"A string, which you must parse; on some locales it will contain a comma as the decimal separator.","why":"Correct. A German or French keyboard shows a comma key, so “8,4” is a perfectly reasonable thing for a user to type."},{"text":"A number on iOS and a string on Android.","why":"Both platforms give you a string. Platform differences show up in which keys appear, not in the type."}],"answer":1},{"q":"<p>Users say Trailhead’s small trash icon is hard to hit, but the design must stay 24 by 24 points. What’s the best fix?</p>","options":[{"text":"Add <code dir=\"ltr\">hitSlop={10}</code> to the Pressable so the touch area grows by 10 points on each side.","why":"Correct. The icon keeps its visual size while the tappable area reaches 44 by 44."},{"text":"Wrap the icon in a second, invisible Pressable.","why":"Two overlapping touch targets make it unclear which one receives the press, and add an extra view for nothing."},{"text":"Switch to <code dir=\"ltr\">onLongPress</code> so users have more time to aim.","why":"Long-press changes the gesture, not the target size. Users still miss, and now they also wait."}],"answer":0},{"q":"<p>What’s wrong with this input?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><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:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setName</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:#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\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onChange</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">setName</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /></span></span></code></pre></div>\n","options":[{"text":"Nothing; <code dir=\"ltr\">onChange</code> and <code dir=\"ltr\">onChangeText</code> are aliases.","why":"They aren’t. <code dir=\"ltr\">onChange</code> receives a native event object, so <code dir=\"ltr\">name</code> would become an object."},{"text":"TextInput can’t be controlled; it must manage its own value.","why":"Controlled inputs are the normal pattern in React Native, exactly as on the web."},{"text":"<code dir=\"ltr\">onChange</code> passes an event, not the text, so <code dir=\"ltr\">name</code> becomes an object; use <code dir=\"ltr\">onChangeText={setName}</code>.","why":"Correct. <code dir=\"ltr\">onChangeText</code> hands you the new string directly."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Turn what a hiker types into whole minutes, or null if it isn't valid.\n// Accept: \"135\", \"2:15\", \"2h 15m\", \"2h15m\", \"1h\", \"45m\" (any case, extra spaces OK).\nfunction parseDurationMin(text) {\n  return Number(text);\n}\n\nconsole.log(parseDurationMin('2h 15m'));\n"},"solution":{"js":"function parseDurationMin(text) {\n  const s = String(text).trim().toLowerCase();\n  let minutes = null;\n  let m;\n  if (/^\\d+$/.test(s)) {\n    minutes = Number(s);\n  } else if ((m = s.match(/^(\\d+):(\\d{2})$/))) {\n    if (Number(m[2]) < 60) minutes = Number(m[1]) * 60 + Number(m[2]);\n  } else if ((m = s.match(/^(?:(\\d+)\\s*h)?\\s*(?:(\\d+)\\s*m)?$/)) && (m[1] || m[2])) {\n    minutes = Number(m[1] ?? 0) * 60 + Number(m[2] ?? 0);\n  }\n  return minutes !== null && minutes > 0 ? minutes : null;\n}\n\nconsole.log(parseDurationMin('2h 15m'));\n"},"checks":[{"id":"plain-minutes","test":"return typeof parseDurationMin === 'function' && parseDurationMin(\"135\") === 135 && parseDurationMin(\" 90 \") === 90;"},{"id":"clock-format","test":"return parseDurationMin(\"2:15\") === 135 && parseDurationMin(\"0:45\") === 45 && parseDurationMin(\"2:75\") === null;"},{"id":"hours-minutes","test":"return parseDurationMin(\"2h 15m\") === 135 && parseDurationMin(\"2h15m\") === 135 && parseDurationMin(\"1H 5M\") === 65;"},{"id":"single-unit","test":"return parseDurationMin(\"1h\") === 60 && parseDurationMin(\"45m\") === 45;"},{"id":"rejects-bad","test":"return parseDurationMin(\"\") === null && parseDurationMin(\"abc\") === null && parseDurationMin(\"0\") === null && parseDurationMin(\"h\") === null;"}],"title":"Parse a hike's duration","prompt":"<p>Trailhead’s form has a <strong>Duration</strong> field with <code dir=\"ltr\">keyboardType=&quot;default&quot;</code>, because hikers type durations in different ways. Write <code dir=\"ltr\">parseDurationMin(text)</code> so it returns the duration in <strong>whole minutes</strong>, or <code dir=\"ltr\">null</code> when the text isn’t valid.</p>\n<p>Accept these shapes (ignoring case and surrounding spaces):</p>\n<ul>\n<li>plain minutes: <code dir=\"ltr\">&quot;135&quot;</code></li>\n<li>clock style: <code dir=\"ltr\">&quot;2:15&quot;</code> (minutes must be below 60, so <code dir=\"ltr\">&quot;2:75&quot;</code> is <code dir=\"ltr\">null</code>)</li>\n<li>hours and minutes: <code dir=\"ltr\">&quot;2h 15m&quot;</code>, <code dir=\"ltr\">&quot;2h15m&quot;</code>, <code dir=\"ltr\">&quot;1h&quot;</code>, <code dir=\"ltr\">&quot;45m&quot;</code></li>\n</ul>\n<p>Return <code dir=\"ltr\">null</code> for empty text, anything else, and for zero.</p>\n","hints":["Normalize first: <code dir=\"ltr\">String(text).trim().toLowerCase()</code>.","Handle each shape with its own regular expression and return early, e.g. <code dir=\"ltr\">/^(\\d+):(\\d{2})$/</code> for the clock style.","For the h/m form, <code dir=\"ltr\">/^(?:(\\d+)\\s*h)?\\s*(?:(\\d+)\\s*m)?$/</code> matches both parts optionally; make sure at least one group actually matched, or an empty string sneaks through."],"explanation":"<p>The function normalizes the text, then tries one pattern per accepted shape, converting each to minutes. Every path that doesn’t match leaves <code dir=\"ltr\">minutes</code> as <code dir=\"ltr\">null</code>, and a final guard rejects zero. Keeping this out of the component means the form only asks one question (<code dir=\"ltr\">parseDurationMin(text) !== null</code>) and you can unit-test every odd input without launching a simulator.</p>\n","checkLabels":{"plain-minutes":"Plain numbers are minutes (“135” → 135)","clock-format":"Clock style works and rejects minutes ≥ 60","hours-minutes":"“2h 15m”, “2h15m” and “1H 5M” are parsed","single-unit":"“1h” → 60 and “45m” → 45","rejects-bad":"Empty, non-numeric and zero inputs return null"}},"words":697,"readMinutes":3}