Semantic-Org / Semantic-UI-React

The official Semantic-UI-React integration
https://react.semantic-ui.com
MIT License
13.22k stars 4.05k forks source link

Dropdown defaultValue with fetched options #1149

Closed kamiky closed 7 years ago

kamiky commented 7 years ago

defaultValue is first undefined and then assigned to the first options, which result to an empty default value

Steps

  1. fetch options via request or just assign defaultValue to undefined then to the first options (with delay)

    var value if (this.state.options.length > 0) { value = this.state.options[0].value } return ( <Dropdown placeholder={this.props.placeholder} options={this.state.options} onChange={this.props.onChange} name={this.props.model} search selection onSearchChange={this.onSearch.bind(this)} defaultValue={value} />

Expected Result

First option shown on the dropdown

Actual Result

Empty dropdown

Version

0.63.6

Testcase

Fork this to show the issue: http://codepen.io/levithomason/pen/ZpBaJX

levithomason commented 7 years ago

In React, the defaultValue is only applied on the first render cycle. Since the first render has an empty value, pending a request, the defaultValue is applied as undefined. Defaults are not considered for future renders so the second render, after receiving the options, will not apply the default.

To apply "initial" values after the first render, use the controlled component pattern and the value prop.

nbkhope commented 7 years ago

@levithomason Could you elaborate on how to set a select dropdown to display a default value that is the first element of an array of objects that is fetched using an asynchronous request? I'm having a hard time with that. I used the following approach:

<Dropdown
  selection
  (...)
  value={(this.props.selectedOption && this.props.selectedOption.value) || fetchedOptions[0].value}
/>

The issue I have right now is when I go to a different page, then go back to the page with the select dropdown (with the state remaining the same) and a new set of options is fetched (not necessarily having the same values), the default value is not set because selectedOption is defined and is matching a selection for the previous set of options. How can I make sure to always make the dropdown select the first element in fetchedOptions by default, no matter what?

levithomason commented 7 years ago

You should do your fetching in an async Redux action. Ideally the dropdown would have a single value assigned from Redux state. Use your reducer initial state for the default value, dispatch an action to fetch the options, and use actions to update the value as well. The idea is that there should be a single value assigned to the dropdown and Redux's initial state, reducer, and actions should manage that state as necessary.

Hope they helps!

gustavonecore commented 6 years ago

@levithomason Thanks for the insight! But currently I'm facing a weird issue with the dropdown. I have my dropdown depending of the props of the wrapper container who haves the redux connection.

This is my dropdown code inside off TransactionRowComponent


<Dropdown
export default class TransactionRowComponent extends Component {
    render () {
        return (
            <Dropdown
                selection
                onChange={(event, data) => {this.props.onChangeBusinessCenter(this.props.transactionId, data.value)} }
                value={this.props.businessCenterId}
                placeholder='Categoría'
                options={this.props.businessCenters.map(bc => {
                    return {key: bc.id, text: bc.name, value: bc.id};
                })}
            />
        );
    }
}

The weird thing is this, at first load if the Dropdown doesn't have a value, you can select one of the list and the text of that selection will be displayed on it like every dropdown does, but if the dropdown have a valid value on it, will be displayed with that option selected, that's ok, but when I try to change to another value, the text is never updated in the main box... but if I open the list options is selected internally...

So I really don't know what I'm doing wrong. This should be straightforward... might be related to my redux/state setup?

That component it's inside of another component (a table) called TransactionComponent

export default class TransactionComponent extends Component {
    renderRows = () => {
        return this.props.items.map(item => {
            return <Table.Row key={item.id}>
                    <Table.Cell>{item.amount}</Table.Cell>
                    <Table.Cell>{item.description}</Table.Cell>
                    <Table.Cell>{moment(item.date).format('DD/MM/YYYY')}</Table.Cell>
                    <Table.Cell>{item.movement_code}</Table.Cell>
                    <Table.Cell>
                        <TransactionRowComponent
                            businessCenters={this.props.businessCenters}
                            transactionId={item.id}
                            businessCenterId={item.business_center.id}
                            onChangeBusinessCenter={this.props.onChangeBusinessCenter}
                        />
                    </Table.Cell>
                </Table.Row>
        });
    }

And this is the main container:

....
changeBusinessCenter = (transactionId, business_center_id) => {
        this.props.actions.transaction.updateTransaction(transactionId, { business_center_id, assets:[] });
    }

    render () {
        let transactions = <TransactionComponent
            items={this.props.transaction.items}
            businessCenters={this.props.business_center.items}
            onChangeBusinessCenter={this.changeBusinessCenter}
        />;

        transactions = this.props.transaction.items.length > 0 ? transactions : [];

        return (
            <Container fluid>
                <Header as='h2'>Transacciones</Header>
                <Grid>
                    <Grid.Column>
                        <label>Desde: </label><DatePicker selected={this.state.startDt} onChange={this.changeStartDt} />
                    </Grid.Column>
                    <Grid.Column>
                        <label>Hasta: </label><DatePicker selected={this.state.endDt} onChange={this.changeEndDt} />
                    </Grid.Column>
                    <Grid.Column>
                        <Button onClick={this.searchTransactions} primary>Buscar</Button>
                    </Grid.Column>
                </Grid>
                { transactions }
            </Container>
        );
    }

Any comment or feedback will be appreciated ;)

gustavonecore commented 6 years ago

Ok... after trying updating the state I was able to get it working.. but the fix doesn't looks ok to me.. In summary, I need to use the state to maintain the changed value...

export default class TransactionRowComponent extends Component {
    state = {
        value:null,
    }
    onChange = (value) => {
        this.setState({value});
        this.props.onChangeBusinessCenter(this.props.transactionId, value)
    }
    render () {
        let value = !this.state.value ? this.props.businessCenterId : this.state.value;
        return (
            <Dropdown
                selection
                fluid
                onChange={(event, data) => {this.onChange(data.value)} }
                value={value}
                placeholder='Categoría'
                options={this.props.businessCenters.map(bc => {
                    return {key: bc.id, text: bc.name, value: bc.id};
                })}
            />
        );
    }
}
brianespinosa commented 6 years ago

@gustavonecore this is more a React question in general. Your assertion that using the state (either in React or something like Redux) to maintain values is correct.

The question is not related to the original bug report. Instead of commenting on old, closed bugs, please open new reports if you have a bug. For usage questions like this, Stack Overflow is a better place to post for help.