react-native: Controlled TextInput broken for Chinese (and other languages) in v0.54 on iOS

Controlled TextInput breaks the Chinese pinyin keyboard’s autocomplete feature. A similar issue was raised a year and a half ago (#8265) and fixed by #7496. However, it appears that others are having the same problem (#12599, #18260, #18379), but re-filing the issue with the template filled out and an example to reproduce.

Note that this works correctly in v0.53. This may have something to do with the big Text, TextInput refactor that dropped with v0.54 (Thanks, btw, @shergin and @hovox!).

I’ve included both a working (v0.53) and broken (v0.54) example below.

Environment

Environment: OS: macOS High Sierra 10.13 Node: 9.3.0 Yarn: 1.3.2 npm: 5.6.0 Watchman: 4.9.0 Xcode: Xcode 9.2 Build version 9C40b Android Studio: Not Found

Packages: (wanted => installed) react: 16.2.0 => 16.2.0 react-native: 0.54.0 => 0.54.0

Expected Behavior

Typing on a US keyboard would bring up Chinese characters corresponding to letters typed.

Actual Behavior

Each new letter typed is considered individually, instead of along with the previous untranslated characters.

Shamelessly stealing an image from @ForU who did a nice job of showing the issue:

image

Steps to Reproduce

Unfortunately, expo hasn’t updated to the latest version of React Native. I’ve prepared a small project that demonstrates the issue here. There is a working version on a branch here; same project, just using RN v0.53 instead of v0.54.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 20
  • Comments: 39 (13 by maintainers)

Commits related to this issue

Most upvoted comments

I updated the pull request and the sample app. And I tried one of Chinese keyboards. I hope it’s expected behavior. screenshot

if it has prop value or defaultvalue,you can’t input Chinese

未命名.gif

@tsangwailam In order to be compatible with android, I change some code. The following code work well for me.

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, TextInput} from 'react-native';


export default class App extends Component {

  state= {
    text:"", // hold the final text
    text2:"", // temporary store the input text
  };

  handleInput = (text) =>  this.setState({ text2: text });
  handleDisplay = (text) =>  this.setState({text:text});

  render() {
    return (
      <View style={styles.container}>
        <TextInput
          value={this.state.text}
          placeholder={'type here'}
          onChangeText={Platform.OS ==='ios' ? this.handleInput : this.handleDisplay}
          onBlur={ Platform.OS ==='ios' ? this.handleDisplay.bind(null,this.state.text2) : null}
        />
        <Text style={styles.text}>{this.state.text}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  input:{
    width:'100%'
  },
  text: {
    textAlign: 'center',
    margin: 20,
    color:'#999'
  },
});

@tsangwailam Thank you. Here is HOC version

withHandleHandWritingTextInput.js

import React, { Component } from 'react';
import {
  Platform,
} from 'react-native';

// https://github.com/facebook/react-native/issues/18403
const withHandleHandWritingTextInput = (WrappedComponent) => {
  class HandleHandWritingTextInput extends React.PureComponent {
    render() {
      const { onChangeText, onBlur, ...rest } = this.props;

      return <WrappedComponent
        onChangeText={text => {
          this.tempText = text;
        }}
        onBlur={e => {
          if (onChangeText) {
            onChangeText(this.tempText);
          }
          if (onBlur) {
            onBlur(e);
          }
        }}
        {...rest}
      />;
    }
  }

  return HandleHandWritingTextInput;
}

export default withHandleHandWritingTextInput;

And in another js use by

import {withHandleHandWritingTextInput} from 'withHandleHandWritingTextInput.js';

const MyTextInput = withHandleHandWritingTextInput(TextInput);

Based on @tsangwailam 's solution, I came up with the following component. It works as a drop-in replacement (almost, except for the ref prop). It would work with external changes to the text value too.

import React, { PureComponent } from 'react';
import { TextInput, Platform } from 'react-native';
import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';

export function fixComposeInput(Component) {
    return class MyTextInput extends PureComponent {
        state = { diffKey: 0, value: '', display: '' };

        static getDerivedStateFromProps(props, state) {

            if (!state || !state.props || props.value !== state.props.value) {

                const value = props.value || '';
                const display = state && state.display || '';
                if (value !== display) {
                    const diffKey = ((state && state.diffKey) >>> 0) + 1;
                    return { props, value, display: value, diffKey };
                }
            }

            return { props };
        }

        handleChange = text => {
            // keep track of the display value
            this.setState({ display: text }, () => {
                const { onChangeText } = this.props;
                onChangeText && onChangeText(text);
            });
        };

        render() {
            const { refInput, value: valueProp, onChangeText, ...inputProps } = this.props;
            const { value, diffKey } = this.state;

            return <Component
                { ...inputProps }
                key={ `TextInput${diffKey}` }
                ref={ refInput }
                value={ value }
                onChangeText={ this.handleChange }
            />;
        }
    };
}

const rnVer = ReactNativeVersion.version.minor;

export default (Platform.OS === 'ios' && rnVer >= 54 && rnVer <= 56 ? fixComposeInput(TextInput) : TextInput);

Currently, i use 2 state properties to solve the problem.

state= {
text:"", // hold the final text
text2:"", // temporary store the input text
}
<TextInput
 value={this.state.text}
  onChangeText={text => {
    this.setState({ text2: text });
  }}
 onBlur={e=>setState({text:this.state.text2})}
/>

@tsangwailam This does not work if you directly trigger submit action before onBlur.

<del>Better solution is introduced here: react native0.54.2以降でTextInputでonChangeTextを使うと日本語変換ができなくなる的なお話 ~ 適当な感じでプログラミングとか!</del>

<TextInput
  value={this.state.text}
  onTextInput={({ nativeEvent: { text } }) => this.setState({ text })}
/>

UPDATE: This solution does not work. It returns only last one text selection as e.nativeEvent.text.

@kevinkevw, but most of times you need to set value. Without a value, how you would implement “reset” functionality, or set initial value? And exactly without a value, you simple do not trigger this bug, because your TextInput now effectively uncontrolled.

I checked it, at first glance Japanese input seems to work. But I using it rarely, so can’t be totally sure. My test code is here: https://github.com/vovkasm/react-native-textinput-bug/tree/rn-0.56 (it is for another bug, so the application tries to filter input, but while filtering is broken, anyone can test anything 😃

I checked my code again this morning and found that the Chinese input method can be entered. The problem has been solved. Thank you very much for your reply. @magicien @danielsuo

0.55.0 has this problem

Thank you @danielsuo! I’m going to update the test plan of the pull request.