TextInputコンポーネントのonChangeText()の入力値をuseRef()で管理したい方へ

## 今回の目的
React Native上での`TextInput`コンポーネントにおいて、`onChangeText()`の入力値を`useState()`で管理していたが、入力するたびに再レンダリングしてしまうため、再レンダリング抑制のために`useRef()`を用いて管理したい。

## 前提条件（環境）
```json
{
  "name": "XXXXXXXXXXXX",
  "version": "1.0.0",
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "@expo/vector-icons": "^13.0.0",
    "@react-native-async-storage/async-storage": "1.18.2",
    "expo": "~49.0.15",
    "expo-constants": "~14.4.2",
    "expo-font": "~11.4.0",
    "expo-linking": "~5.0.2",
    "expo-module-scripts": "^3.1.0",
    "expo-router": "^2.0.0",
    "expo-status-bar": "~1.6.0",
    "firebase": "^10.7.1",
    "react": "18.2.0",
    "react-native": "0.72.6",
    "react-native-gesture-handler": "~2.12.0",
    "react-native-safe-area-context": "4.6.3",
    "react-native-screens": "~3.22.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@stylistic/eslint-plugin": "^1.5.3",
    "@types/react": "~18.2.14",
    "@typescript-eslint/eslint-plugin": "^6.17.0",
    "eslint": "^8.56.0",
    "eslint-config-standard-with-typescript": "^43.0.0",
    "eslint-plugin-import": "^2.29.1",
    "eslint-plugin-n": "^16.6.1",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-react": "^7.33.2",
    "typescript": "^5.3.3"
  },
  "private": true
}

```

## 作成したソースコード
`SubmitButton`を押下するまでの再レンダリング抑制が目的のため、一部処理を目隠し目的で修正しております。

【改修前】
```TypeScript
import {
    View,
    Text,
    StyleSheet,
    TextInput,
    TouchableOpacity,
} from "react-native";
import { SubmitButton } from "../../components/SubmitButton";
import { Link, router } from "expo-router";
import { useState } from "react";

type SignUpProps = {
    email: string;
    password: string;
};

const SignUp = (): JSX.Element => {
    const { container, inner, title, input, footer, footerLink, footerText } =
        styles;

    const [email, setEmail] = useState<string>("");
    const [password, setPassword] = useState<string>("");

    const handlePress = (props: SignUpProps): void => {
        const { email, password } = props;
        // 会員登録
        console.log(email, password);

        router.push("memo/List");
    };

    return (
        <View style={container}>
            <View style={inner}>
                <Text style={title}>Sign Up</Text>
                <TextInput
                    style={input}
                    value={email}
                    autoCapitalize="none"
                    keyboardType="email-address"
                    placeholder="Email Address"
                    textContentType="emailAddress"
                    onChangeText={(text: string) => {
                        setEmail(text);
                    }}
                />
                <TextInput
                    style={input}
                    value={password}
                    autoCapitalize="none"
                    secureTextEntry
                    placeholder="Password"
                    textContentType="password"
                    onChangeText={(text: string) => {
                        setPassword(text);
                    }}
                />
                <SubmitButton
                    displayLabelValue="Submit"
                    onPress={() => {
                        handlePress({
                            email: emailRef.current,
                            password: passwordRef.current,
                        });
                    }}
                />
                <View style={footer}>
                    <Text style={footerText}>Already registered?</Text>
                    {/* Linkタブに子要素を含めるためにはasChildが必須 */}
                    <Link href="auth/Login" asChild replace>
                        <TouchableOpacity>
                            <Text style={footerLink}>Log In</Text>
                        </TouchableOpacity>
                    </Link>
                </View>
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#f0f4f8",
    },
    inner: {
        paddingVertical: 24,
        paddingHorizontal: 27,
    },
    title: {
        fontSize: 24,
        lineHeight: 32,
        fontWeight: "bold",
        marginBottom: 24,
    },
    input: {
        borderWidth: 1,
        borderColor: "#dddddd",
        backgroundColor: "#ffffff",
        height: 48,
        padding: 8,
        fontSize: 16,
        marginBottom: 16,
    },
    footer: {
        flexDirection: "row",
    },
    footerText: {
        fontSize: 14,
        lineHeight: 24,
        marginRight: 8,
        color: "#000000",
    },
    footerLink: {
        fontSize: 14,
        lineHeight: 24,
        color: "#467fd3",
    },
});

export default SignUp;
```
【改修後】
```TypeScript
import {
    View,
    Text,
    StyleSheet,
    TextInput,
    TouchableOpacity,
} from "react-native";
import { SubmitButton } from "../../components/SubmitButton";
import { Link, router } from "expo-router";
import { useRef } from "react";

type SignUpProps = {
    email: string;
    password: string;
};

const SignUp = (): JSX.Element => {
    const { container, inner, title, input, footer, footerLink, footerText } =
        styles;

    const emailRef = useRef<string>("");
    const passwordRef = useRef<string>("");

    const handlePress = (props: SignUpProps): void => {
        const { email, password } = props;
        // 会員登録
        console.log(email, password);

        router.push("memo/List");
    };

    return (
        <View style={container}>
            <View style={inner}>
                <Text style={title}>Sign Up</Text>
                <TextInput
                    style={input}
                    value={emailRef.current}
                    autoCapitalize="none"
                    keyboardType="email-address"
                    placeholder="Email Address"
                    textContentType="emailAddress"
                    onChangeText={(text: string) => {
                        emailRef.current = text;
                    }}
                />
                <TextInput
                    style={input}
                    value={passwordRef.current}
                    autoCapitalize="none"
                    secureTextEntry
                    placeholder="Password"
                    textContentType="password"
                    onChangeText={(text: string) => {
                        passwordRef.current = text;
                    }}
                />
                <SubmitButton
                    displayLabelValue="Submit"
                    onPress={() => {
                        handlePress({
                            email: emailRef.current,
                            password: passwordRef.current,
                        });
                    }}
                />
                <View style={footer}>
                    <Text style={footerText}>Already registered?</Text>
                    {/* Linkタブに子要素を含めるためにはasChildが必須 */}
                    <Link href="auth/Login" asChild replace>
                        <TouchableOpacity>
                            <Text style={footerLink}>Log In</Text>
                        </TouchableOpacity>
                    </Link>
                </View>
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#f0f4f8",
    },
    inner: {
        paddingVertical: 24,
        paddingHorizontal: 27,
    },
    title: {
        fontSize: 24,
        lineHeight: 32,
        fontWeight: "bold",
        marginBottom: 24,
    },
    input: {
        borderWidth: 1,
        borderColor: "#dddddd",
        backgroundColor: "#ffffff",
        height: 48,
        padding: 8,
        fontSize: 16,
        marginBottom: 16,
    },
    footer: {
        flexDirection: "row",
    },
    footerText: {
        fontSize: 14,
        lineHeight: 24,
        marginRight: 8,
        color: "#000000",
    },
    footerLink: {
        fontSize: 14,
        lineHeight: 24,
        color: "#467fd3",
    },
});

export default SignUp;
```
## 発生した問題
画面上に表示されている`TextInput`コンポーネント部分に値を入力しても、「入力した値」しか反映されなくなってしまった。
（例）"abc"を入力すると"abc"と値が保持、表示されているはずが、最後の入力である"c"のみしか値が保持されず、表示もされなくなってしまった。

## 試したこと
`TextInput`コンポーネントにRef属性を追加し、内部から入力されている値を取得しようとした。

## 解決方法
`TextInput`コンポーネントにvalue属性を追加していたが、それを削除したところ意図した入力値の保持、表示が確認できた。
```TypeScript
import {
    View,
    Text,
    StyleSheet,
    TextInput,
    TouchableOpacity,
} from "react-native";
import { SubmitButton } from "../../components/SubmitButton";
import { Link, router } from "expo-router";
import { useRef } from "react";

type SignUpProps = {
    email: string;
    password: string;
};

const SignUp = (): JSX.Element => {
    const { container, inner, title, input, footer, footerLink, footerText } =
        styles;

    const emailRef = useRef<string>("");
    const passwordRef = useRef<string>("");

    const handlePress = (props: SignUpProps): void => {
        const { email, password } = props;
        // 会員登録
        console.log(email, password);

        router.push("memo/List");
    };

    return (
        <View style={container}>
            <View style={inner}>
                <Text style={title}>Sign Up</Text>
                <TextInput
                    style={input}
                    autoCapitalize="none"
                    keyboardType="email-address"
                    placeholder="Email Address"
                    textContentType="emailAddress"
                    onChangeText={(text: string) => {
                        emailRef.current = text;
                    }}
                />
                <TextInput
                    style={input}
                    autoCapitalize="none"
                    secureTextEntry
                    placeholder="Password"
                    textContentType="password"
                    onChangeText={(text: string) => {
                        passwordRef.current = text;
                    }}
                />
                <SubmitButton
                    displayLabelValue="Submit"
                    onPress={() => {
                        handlePress({
                            email: emailRef.current,
                            password: passwordRef.current,
                        });
                    }}
                />
                <View style={footer}>
                    <Text style={footerText}>Already registered?</Text>
                    {/* Linkタブに子要素を含めるためにはasChildが必須 */}
                    <Link href="auth/Login" asChild replace>
                        <TouchableOpacity>
                            <Text style={footerLink}>Log In</Text>
                        </TouchableOpacity>
                    </Link>
                </View>
            </View>
        </View>
    );
};

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#f0f4f8",
    },
    inner: {
        paddingVertical: 24,
        paddingHorizontal: 27,
    },
    title: {
        fontSize: 24,
        lineHeight: 32,
        fontWeight: "bold",
        marginBottom: 24,
    },
    input: {
        borderWidth: 1,
        borderColor: "#dddddd",
        backgroundColor: "#ffffff",
        height: 48,
        padding: 8,
        fontSize: 16,
        marginBottom: 16,
    },
    footer: {
        flexDirection: "row",
    },
    footerText: {
        fontSize: 14,
        lineHeight: 24,
        marginRight: 8,
        color: "#000000",
    },
    footerLink: {
        fontSize: 14,
        lineHeight: 24,
        color: "#467fd3",
    },
});

export default SignUp;
```
## 補足
今回は`TextInput`コンポーネントのvalue属性を削除したが、現環境（バージョン）下での解決方法である可能性もある。（value属性がなくても画面上に表示されているのが驚きであり新しく発見したため）
バージョンが新しくなった際に、以上の対応では解決しない恐れもあるため、こちらでも随時確認していく。