Created
May 12, 2021 07:14
-
-
Save Noitidart/fcb79f97e86a923f285b1448de85fdd1 to your computer and use it in GitHub Desktop.
Non-re-rendering react-hook-form TextInput component for react-native.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react'; | |
import { Text, View, StyleSheet, TextInput, Button, Alert } from 'react-native'; | |
import { useForm, useController } from 'react-hook-form'; | |
import Constants from 'expo-constants'; | |
import { pick, omit, defaults } from 'lodash'; | |
const REQUIRED = { required: true }; | |
function ClassicHookedTextInput(props) { | |
const controllerProps = defaults( | |
pick(props, 'control', 'defaultValue', 'name', 'rules'), | |
{ defaultValue: '' } | |
); | |
const textInputProps = omit(props, Object.keys(controllerProps)); | |
const controller = useController(controllerProps); | |
return ( | |
<> | |
{useRenderCounter('Classic')} | |
<TextInput | |
{...textInputProps} | |
onChangeText={controller.field.onChange} | |
onBlur={controller.field.onBlur} | |
/> | |
</> | |
); | |
} | |
function DirectHookedTextInput(props) { | |
const controllerProps = defaults( | |
pick(props, 'control', 'defaultValue', 'name', 'rules'), | |
{ defaultValue: '' } | |
); | |
const textInputProps = omit(props, Object.keys(controllerProps)); | |
const controller = useController(controllerProps); | |
const lastChangedText = React.useRef(controllerProps.defaultValue); | |
const handleChangeText = React.useCallback((text) => { | |
lastChangedText.current = text; | |
controller.field.onChange(text); | |
}, []); | |
const textInputRef = React.useRef(); | |
// Set value of TextInput if value is changed externally (not due to TextInput.onChangeText) | |
React.useEffect(() => { | |
console.log( | |
`controller.field.value changed to: "${controller.field.value}". lastChangedText is: "${lastChangedText.current}".` | |
); | |
if (lastChangedText.current !== controller.field.value) { | |
// Doing setNativeProps does not trigger TextInput.onChangeText, so set it here | |
lastChangedText.current = controller.field.value; | |
textInputRef.current && | |
textInputRef.current.setNativeProps({ text: controller.field.value }); | |
} | |
}, [controller.field.value]); | |
const handleBlur = React.useCallback((e) => { | |
controller.field.onBlur(e); | |
}, []); | |
return ( | |
<> | |
{useRenderCounter('DirectController')} | |
<DirectTextInput | |
{...textInputProps} | |
ref={textInputRef} | |
onChangeText={handleChangeText} | |
onBlur={handleBlur} | |
/> | |
</> | |
); | |
} | |
const DirectTextInput = React.memo( | |
React.forwardRef((props, ref) => ( | |
<> | |
{useRenderCounter('Direct')} | |
<TextInput {...props} ref={ref} /> | |
</> | |
)) | |
); | |
function useRenderCounter(label = '') { | |
const inputRef = React.useRef(); | |
const savedCount = React.useRef(0); | |
React.useEffect(() => { | |
savedCount.current++; | |
inputRef.current?.setNativeProps({ | |
text: label + (label ? ': ' : '') + savedCount.current.toString(), | |
}); | |
}); | |
return ( | |
<TextInput | |
style={{ | |
alignSelf: 'flex-start', | |
backgroundColor: '#ccc', | |
borderRadius: 4, | |
paddingHorizontal: 4, | |
paddingVertical: 2, | |
marginHorizontal: 6, | |
fontSize: 12, | |
// position: 'absolute', | |
transform: [{ translateX: -4 }, { translateY: -4 }], | |
}} | |
pointerEvents="none" | |
defaultValue={savedCount.current.toString()} | |
ref={inputRef} | |
/> | |
); | |
} | |
export default () => { | |
const form = useForm(); | |
const handleSubmit = (data) => console.log(data); | |
return ( | |
<View style={styles.container}> | |
<Text style={styles.label}>First name</Text> | |
<ClassicHookedTextInput | |
control={form.control} | |
name="firstName" | |
rules={REQUIRED} | |
style={styles.input} | |
/> | |
<Text style={styles.label}>Last name</Text> | |
<DirectHookedTextInput | |
control={form.control} | |
name="lastName" | |
rules={REQUIRED} | |
style={styles.input} | |
/> | |
<View style={styles.button}> | |
<Button | |
style={styles.buttonInner} | |
color | |
title="Reset" | |
onPress={() => { | |
form.reset({ | |
firstName: 'Bill', | |
lastName: 'Luo', | |
}); | |
}} | |
/> | |
</View> | |
<View style={styles.button}> | |
<Button | |
style={styles.buttonInner} | |
color | |
title="Button" | |
onPress={form.handleSubmit(handleSubmit)} | |
/> | |
</View> | |
</View> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
label: { | |
color: 'white', | |
margin: 20, | |
marginLeft: 0, | |
}, | |
button: { | |
marginTop: 40, | |
color: 'white', | |
height: 40, | |
backgroundColor: '#ec5990', | |
borderRadius: 4, | |
}, | |
container: { | |
flex: 1, | |
justifyContent: 'center', | |
paddingTop: Constants.statusBarHeight, | |
padding: 8, | |
backgroundColor: '#0e101c', | |
}, | |
input: { | |
backgroundColor: 'white', | |
borderColor: 'none', | |
height: 40, | |
padding: 10, | |
borderRadius: 4, | |
}, | |
}); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment