React-native: Picker Component resets when items list changes

Created on 6 Apr 2017  路  43Comments  路  Source: facebook/react-native

Description

If the picker item list changes, then, in some cases, the picker will call the onValueChange callback with the first item in the list as it's argument.

Reproduction Steps and Sample Code

This can be reproduced at https://snack.expo.io/Sy1JClEag.

Select 2 or 3, then click on exclude. The new value is 1. This does not happen if you select a higher value than the one being excluded

Solution

Do not call the onValueChange callback when the items list changes, but the selectedValue is stil present in the list

Additional Information

  • React Native version: 0.42
  • Platform: Android
  • Development Operating System: Linux
  • Dev tools: Android SDK version 23
Bug Help Wanted Android Ran Commands Locked

Most helpful comment

@stale This is still an issue.

@syamjayaraj's link to a "solution" that is "working perfectly" has nothing to do with this issue. That solution refers to changing the selection of a Picker, not editing an Option for the Picker and having the current value reset after making that edit.

So, yes, this is still an issue in the react-native core and should remain on here and get some attention from the react-native team.

All 43 comments

Did you find a work around for this?

Nope. In theory, I think it should be doable, but it seems overly complicated. In my case, I managed to get rid of the need to change the picker items.

Got snagged with this as well. I am renaming my items in the Picker and when you update the Picker items, including the renamed one, it loses the selected value, even if your selected value is still in the list.

For example:

<Picker selectedValue="2" ...>
  <Picker.Item key="1" value="1" label="One" />
  <Picker.Item key="2" value="2" label="Two" />
  <Picker.Item key="3" value="3" label="Three" />
</Picker>

If you change the second Picker.Item to:

<Picker.Item key="2" value="2" label="Second" />

It has to re-render but it loses the selectedValue. The selectedValue just becomes the first one, even though the value of the selected item hasn't changed, just the label.

Might be a little confusing but it's a real pain when renaming things. For now, I am getting around it with a dirty solution that adds a new Picker.Item to the list instead of renaming one, which seems to work fine. Except that now you have an invalid Picker.Item in the list.

<Picker selectedValue="2" ...>
  <Picker.Item key="1" value="1" label="One" />
  <Picker.Item key="2" value="2" label="Two" />
  <Picker.Item key="3" value="3" label="Three" />
  <Picker.Item key="2" value="2" label="Second" />
</Picker>

This resolved for me:

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

class MyPicker extends Component {

  componentWillReceiveProps(nextProps) {
    if (Platform.OS === 'android') {
      let selectedIndex = -1;
      nextProps.children.forEach((c, i) => {
        if (c.props.value === nextProps.selectedValue) {
          selectedIndex = i;
        }
      });
      this.refs.picker._reactInternalInstance._renderedComponent._instance.setState({ initialSelectedIndex: selectedIndex, selectedIndex });
    }
  }

  onValueChange(value, index) {
    if (value !== this.props.selectedValue) {
      this.props.onValueChange(value, index);
    }
  }

  render() {
    return (
      <Picker ref='picker' {...this.props} onValueChange={this.onValueChange.bind(this)}>
        {this.props.children}
      </Picker>
    );
  }
}

MyPicker.Item = Picker.Item;

export { MyPicker };

@goblinbr Thanks for the solution, but for some reason it didn't help me.

Honestly, I'm a little surprised that a major bug such as this attracts so little attention. I just started working with the Picker component and immediately stumbled upon it. It is easily reproducible on the latest 0.47 version on both the emulator and the actual Android 6.0 phone despite the fact that #9220 was closed a couple of weeks ago.

@goblinbr Thank you for you solution, but it doesn't work on me as well. This would not stop triggering the "onValueChange". Is there any other walkaround?

Having the same issue. React Native 0.48.2

I would love it if an core member would jump in here and give their thoughts. That would be enough for myself or somebody else to work on a PR. But working on a PR without a core member's encouragement is a risky use of time.

Never mind, the issue was on my side, I has two onValueChange attributes declared on my component. :man_facepalming:

@hirvesh So you're saying this is no longer an issue on 0.48.2?

I've just tried it on 0.48.2 - same problem.

In my scenario, I have a component with multiple Picker elements and one of them changes the interface language. When I change the language and save the settings, the component re-renders and some Picker elements receive new labels. All numeric Pickers retain the correct states, but all text-based ones are reset to the first element.

Probably @Hirvesh's use case is different.

I get this bug as well on react native 0.44.3. I traced it to the event argument in _onChange in PickerAndroid.android, but I can't see any difference between the event arg when the option is actually being selected and when the options are changing.

Could one of the core devs please look into this?

Still happening in 0.49. This Picker component is so full of bugs that I wonder if it'd make sense to just rewrite it.

Has to coerce into string for proper comparison

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

@stale This is still an issue.

@syamjayaraj's link to a "solution" that is "working perfectly" has nothing to do with this issue. That solution refers to changing the selection of a Picker, not editing an Option for the Picker and having the current value reset after making that edit.

So, yes, this is still an issue in the react-native core and should remain on here and get some attention from the react-native team.

I took a look at this, and it seems to be expected behavior. From the PickerAndroid.android.js source code,

// The picker is a controlled component. This means we expect the
// on*Change handlers to be in charge of updating our
// `selectedValue` prop. That way they can also
// disallow/undo/mutate the selection of certain values. In other
// words, the embedder of this component should be the source of
// truth, not the native component.

Therefore, @goblinbr's solution is correct.

@hramos Thanks for checking out this thread, but I doubt that this could be expected behaviour. Have you seen my use case?

In my scenario, I have a component with multiple Picker elements and one of them changes the interface language. When I change the language and save the settings, the component re-renders and some Picker elements receive new labels. All numeric Pickers retain the correct states, but all text-based ones are reset to the first element.

Note that I only update the labels, not values, and there is no reason for the component to touch selectedValue, let alone reset it to the first option.

Also, as far as I can tell, the solution you're referring to didn't help anyone in this thread.

Hey @hramos

I can actually confirm the usecase of @denisftw because I have the exact same problem on the exact same usecase. I want my users to be able to select the app language.

My code looks like:


const translateLanguage = lang => {
  formatMessage({ id: `preferences.language.${lang}` });
};

class LanguageSection extends React.PureComponent {
    render() {
    return (
      <LanguageConsumer>
        {(language, updateLanguage) => {
          return (
            <PreferenceSection name="language">
              <View padding={Spacing}>
                <Picker
                  primary
                  values={AVAILABLE_LANGUAGES}
                  label={translateLanguage}
                  selectedValue={language}
                  onValueChange={updateLanguage}
                />
              </View>
            </PreferenceSection>
          );
        }}
      </LanguageConsumer>
    );
  }
}

And I get the onValueChange triggered 3 times everytime I select a new language (first with the newly selected language, then with the default language.

Now, if I use hardcoded labels (ie, the language options are not translated according to current app language), it works fine:

const translateLanguage = lang => {
  switch (lang) {
    case 'en':
      return 'English';
    case 'fr':
      return 'Fran莽ais';
    default:
      throw new Error(`Unknown language ${lang}`);
  }
};

So it looks to me there is an issue: if after selection an option, the picker option label changes, we get onValueChange triggered multiple times for unknown reasons

Having the same issue on React Native 0.55.4. It only happens when sending new Picker.Item lists

same here

It would appear that the Picker is looking for PropType.number and acts up if you pass non integer values to value. I switched the the value to a number instead of an object (because why not save the Picker.Item value as an object that you are working with from the array amirite?) and it starting acting right. In the post above... they are passing a string ("1", "2", etc.) as the value. Try switching it to an actual type number and see if you get the same success that I did. >>> Opinion/advice: Obviously you would need to change PropTypes and defaults to follow suit as you should be using props and not state.

static defaultProps = {
        label: 'Label',
        isFilterable: false,
        hasLabel: false,
        options: [],
        selectedValue: 0,
        style: {},
        testID: 'cvPickerTestId',
        enabled: true,
        mode: 'dropdown',
        prompt: '',
        itemStyle: {},
        pickerChange: () => Promise.resolve(),
    };

    static propTypes = {
        label: PropTypes.string,
        hasLabel: PropTypes.bool,
        isFilterable: PropTypes.bool,
        options: PropTypes.array,
        selectedValue: PropTypes.number,
        style: PropTypes.object,
        testID: PropTypes.string,
        enabled: PropTypes.bool,
        mode: PropTypes.string,
        prompt: PropTypes.string,
        itemStyle: PropTypes.object,
        pickerChange: PropTypes.func,
    };

    constructor(props) {
        super(props);
        const { options } = props;
        this.state = { pickerOptions: options };
    }

    render() {
        const { isFilterable, hasLabel } = this.props;
        const structure = cvPicker();

        return (
            <View style={ structure.cvPickerHolder }>
                { hasLabel && this.renderLabel() }
                { isFilterable ? this.renderPickerWithFilter() : this.renderPicker() }
            </View>
        );
    }

    renderLabel = () => {
        const { label } = this.props;
        const structure = cvPicker();

        return (
            <View style={ structure.cvPickerLabel }>
                <Text>{ label }</Text>
            </View>
        );
    }

    renderPicker = () => {
        const { options: propOptions, isFilterable, pickerChange, style, ...restProps } = this.props;
        const { pickerOptions } = this.state;

        return (
            <Picker
                { ...restProps }
                style={ style }
                onValueChange={ this.handleValueChange }>
                { pickerOptions.map((item, i) => this.renderPickerItem(item, i)) }
            </Picker>
        );
    }

    renderPickerWithFilter = () => {
        const { options: propOptions, isFilterable, pickerChange, style, ...restProps } = this.props;
        const { pickerOptions } = this.state;
        const structure = cvPicker();

        return (
            <View style={ structure.cvPickerContainer }>
                <View style={ structure.cvPickerFilter }>
                    <TextInput
                        style={ structure.cvPickerFilterTextInput }
                        placeholder="Filter options..."
                        onChangeText={ this.handleTextChange }
                        underlineColorAndroid="transparent" />
                </View>
                <View style={ structure.cvPickerView }>
                    <Picker
                        { ...restProps }
                        style={ style }
                        onValueChange={ this.handleValueChange }>
                        { pickerOptions.map((item, i) => this.renderPickerItem(item, i)) }
                    </Picker>
                </View>
            </View>
        );
    }

    renderPickerItem = (item) => {
        const { itemStyle } = this.props;
        const { value, label, index, key } = item;
        const structure = cvPicker();

        return (
            <Picker.Item
                key={ `${index}-${key}` }
                style={ [ structure.cvPickerItem, itemStyle ] }
                label={ label }
                value={ value } />
        );
    }

    handleTextChange = (query) => {
        const newState = this.state;
        const { options: propOptions } = this.props;
        const filteredOptions = propOptions.filter((item) => {
            if (item.label.indexOf(query) > -1) {
                return item;
            }
            return false;
        });
        newState.pickerOptions = filteredOptions;
        this.setState(newState);
    }

    handleValueChange = (item) => {
        const { pickerChange, options } = this.props;
        const returnItem = options.find(x => parseInt(x.value, 10) === item);
        pickerChange(returnItem);
    }

The last solution didn't fix it for me - however it made me realize what was going on.

In my case, there was an event that was changing the Picker.Items' labels. That was then causing the currently selected item to reset.

Here's a simple workaround I'm using. This might not suit all usages.

The key point is when you know the options' label will be changed, force React to create a new Picker node instead of updating the existing one.

// inside a component which renders Picker
render() {
  // say you know the change of UI language will cause the labels to be changed
  return (
    <div>
        <Picker
            key=`gender-${language}`
        >
            {genderOptions}
        </Picker>
        <Picker
            key=`month-${language}`
        >
            {birthMonthOptions}
        </Picker>
    </div>
  );
}

@jcppman Interesting. I wonder if you could use a SHA hash or some unique ID for the Array of options, and keep that in the Picker's key prop. Then whenever the values change at all, it would cause a re-render of the entire Picker component.

Very similar to how assets are hashed in web servers, like Rails.

@joshuapinter Thanks for the inspiration. yeah, it works quite well for my use case:

function MyPicker(props) {
  const key = React.Children.map(props.children, (c) => {
    return Object.values(c.props).join(',');
  }).join(';');
  return <Picker {...props} key={key} />;
}

Now the workaround is generic and simple!

@jcppman Nice job! Can't wait to give this a go and see how it works. 馃憤

You can try this. this worked for. At first it kept going back to the selected value.

onValueChange (value) { this.setState({ selected1 : value }); }
selectedValue={this.state.selected1} onValueChange={this.onValueChange.bind(this)}>

@goblinbr alternative component version to fix this problem with deep-equal npm package

```import React, { Component } from 'react'
import { Picker, Platform } from 'react-native'
import equal from 'deep-equal'

class FixedPicker extends Component {
disabledOnValueChange = false

componentWillReceiveProps (nextProps) {
if (Platform.OS === 'android') {
const thisChildrenProps = this.props.children.map(child => child.props)
const nextChildrenProps = nextProps.children.map(child => child.props)

  if (equal(thisChildrenProps, nextChildrenProps) === false) this.disabledOnValueChange = true
}

}

onValueChange = (value, index) => {
if (this.disabledOnValueChange === true) {
this.disabledOnValueChange = false
return
}

this.props.onValueChange(value, index)

}

render() {
return (
ref='picker'
{...this.props}
onValueChange={this.onValueChange}
>
{this.props.children}

)
}
}

FixedPicker.Item = Picker.Item

export { FixedPicker }

Hey @jcppman, that worked great!!

For those that might want this in a Component form, here's the modified version.

import React      from "react";
import { Picker } from "react-native";

// This is necessary because the native Picker does not handle edits of existing Picker.Items.
// For example, if you edit the selected Picker.Item, it will lose the selected value. This fixes that.
//
// See https://github.com/facebook/react-native/issues/13351#issuecomment-450281257
//
export default class FixedPicker extends React.Component {

  render() {
    const key = React.Children.map( this.props.children, child => Object.values( child.props ).join( "," ) ).join( ";" );

    return <Picker { ...this.props } key={ key } />;
  }

}

Thanks again, @jcppman!

Thanks @jcppman and @joshuapinter ! Modifications at multiple places was made simple by the modified version !

@jcppman I wasted hours on this bug, at first I thought it came from react-18next (my use case is the same of yours, a Picker to switch app language). Thanks a lot!

Honestly, seeing open issue this big from 2017 is worring me a lot, is React-native still in active development and REAL bugfixing?

@antonioaltamura Are you kidding me? There's been 4 commits in the last 12 hours.

Screen Shot 2019-03-16 at 12 52 48 PM

Don't get me wrong, I'm as frustrated as you are that these seemingly glaring bugs are still not fixed in master after several years and a dozen releases, but a quick look at the repo shows it's under very active development.

I was able to repro this on 0.59. If someone wants to investigate this problem and send a PR that would be greatly appreciated.

cc @Kudo

@cpojer Here is the PR https://github.com/facebook/react-native/pull/24793 to address this issue.
Controlled component is hard, hopefully if there could be some systematical solution.

same issue here

same issue.

Same issue here.

There is this naming trouble, be careful, prop selectedValue accepts the index of the selected value.

Was this page helpful?
0 / 5 - 0 ratings