Improve textinput performance in login and account creation (#4673)

* Change login form to use uncontrolled inputs

* Debounce state updates in account creation to reduce flicker

* Refactor state-control of account creation forms to fix perf without relying on debounces

* Remove canNext and enforce is13

* Re-add live validation to signup form (#4720)

* Update validation in real time

* Disable on invalid

* Clear server error on typing

* Remove unnecessary clearing of error

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
Paul Frazee
2024-07-02 14:43:34 -07:00
committed by GitHub
parent 4bb4452f08
commit 63bb8fda2d
7 changed files with 357 additions and 269 deletions
+138 -80
View File
@@ -1,56 +1,96 @@
import React from 'react'
import React, {useRef} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {
createFullHandle,
IsValidHandle,
validateHandle,
} from '#/lib/strings/handles'
import {logEvent} from '#/lib/statsig/statsig'
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
import {useAgent} from '#/state/session'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {useSignupContext} from '#/screens/Signup/state'
import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
import {atoms as a, useTheme} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {BackNextButtons} from './BackNextButtons'
export function StepHandle() {
const {_} = useLingui()
const t = useTheme()
const {state, dispatch} = useSignupContext()
const submit = useSubmitSignup({state, dispatch})
const agent = useAgent()
const handleValueRef = useRef<string>(state.handle)
const [draftValue, setDraftValue] = React.useState(state.handle)
const [validCheck, setValidCheck] = React.useState<IsValidHandle>({
handleChars: false,
hyphenStartOrEnd: false,
frontLength: false,
totalLength: true,
overall: false,
})
const onNextPress = React.useCallback(async () => {
const handle = handleValueRef.current.trim()
dispatch({
type: 'setHandle',
value: handle,
})
useFocusEffect(
React.useCallback(() => {
setValidCheck(validateHandle(state.handle, state.userDomain))
}, [state.handle, state.userDomain]),
)
const newValidCheck = validateHandle(handle, state.userDomain)
if (!newValidCheck.overall) {
return
}
const onHandleChange = React.useCallback(
(value: string) => {
if (state.error) {
dispatch({type: 'setError', value: ''})
}
try {
dispatch({type: 'setIsLoading', value: true})
dispatch({
type: 'setHandle',
value,
const res = await agent.resolveHandle({
handle: createFullHandle(handle, state.userDomain),
})
},
[dispatch, state.error],
)
if (res.data.did) {
dispatch({
type: 'setError',
value: _(msg`That handle is already taken.`),
})
return
}
} catch (e) {
// Don't have to handle
} finally {
dispatch({type: 'setIsLoading', value: false})
}
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
phoneVerificationRequired:
state.serviceDescription?.phoneVerificationRequired,
})
// phoneVerificationRequired is actually whether a captcha is required
if (!state.serviceDescription?.phoneVerificationRequired) {
submit()
return
}
dispatch({type: 'next'})
}, [
_,
dispatch,
state.activeStep,
state.serviceDescription?.phoneVerificationRequired,
state.userDomain,
submit,
agent,
])
const onBackPress = React.useCallback(() => {
const handle = handleValueRef.current.trim()
dispatch({
type: 'setHandle',
value: handle,
})
dispatch({type: 'prev'})
logEvent('signup:backPressed', {
activeStep: state.activeStep,
})
}, [dispatch, state.activeStep])
const validCheck = validateHandle(draftValue, state.userDomain)
return (
<ScreenTransition>
<View style={[a.gap_lg]}>
@@ -59,9 +99,17 @@ export function StepHandle() {
<TextField.Icon icon={At} />
<TextField.Input
testID="handleInput"
onChangeText={onHandleChange}
onChangeText={val => {
if (state.error) {
dispatch({type: 'setError', value: ''})
}
// These need to always be in sync.
handleValueRef.current = val
setDraftValue(val)
}}
label={_(msg`Input your user handle`)}
defaultValue={state.handle}
defaultValue={draftValue}
autoCapitalize="none"
autoCorrect={false}
autoFocus
@@ -69,59 +117,69 @@ export function StepHandle() {
/>
</TextField.Root>
</View>
<Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(state.handle, state.userDomain)}
{draftValue !== '' && (
<Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(draftValue, state.userDomain)}
</Text>
</Text>
</Text>
)}
<View
style={[
a.w_full,
a.rounded_sm,
a.border,
a.p_md,
a.gap_sm,
t.atoms.border_contrast_low,
]}>
{state.error ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={false} />
<Text style={[a.text_md, a.flex_1]}>{state.error}</Text>
</View>
) : undefined}
{validCheck.hyphenStartOrEnd ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.handleChars} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Only contains letters, numbers, and hyphens</Trans>
</Text>
</View>
) : (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.hyphenStartOrEnd} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Doesn't begin or end with a hyphen</Trans>
</Text>
</View>
)}
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon
valid={validCheck.frontLength && validCheck.totalLength}
/>
{!validCheck.totalLength ? (
<Text style={[a.text_md, a.flex_1]}>
<Trans>No longer than 253 characters</Trans>
</Text>
{draftValue !== '' && (
<View
style={[
a.w_full,
a.rounded_sm,
a.border,
a.p_md,
a.gap_sm,
t.atoms.border_contrast_low,
]}>
{state.error ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={false} />
<Text style={[a.text_md, a.flex_1]}>{state.error}</Text>
</View>
) : undefined}
{validCheck.hyphenStartOrEnd ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.handleChars} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Only contains letters, numbers, and hyphens</Trans>
</Text>
</View>
) : (
<Text style={[a.text_md, a.flex_1]}>
<Trans>At least 3 characters</Trans>
</Text>
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.hyphenStartOrEnd} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Doesn't begin or end with a hyphen</Trans>
</Text>
</View>
)}
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon
valid={validCheck.frontLength && validCheck.totalLength}
/>
{!validCheck.totalLength ? (
<Text style={[a.text_md, a.flex_1]}>
<Trans>No longer than 253 characters</Trans>
</Text>
) : (
<Text style={[a.text_md, a.flex_1]}>
<Trans>At least 3 characters</Trans>
</Text>
)}
</View>
</View>
</View>
)}
</View>
<BackNextButtons
isLoading={state.isLoading}
isNextDisabled={!validCheck.overall}
onBackPress={onBackPress}
onNextPress={onNextPress}
/>
</ScreenTransition>
)
}