Stretch0
Stretch0

Reputation: 9251

What is the best way to handle sorting data that is passed from props?

It is considered bad practice to assign props to a components state. But sometimes this seems necessary so that you can mutate that state and have the component rerender.

Currently my source of data is passed in from props, then I want to assign it to state in my constructor so that it defaults. Then when an button is clicked, I want to sort / filter that data then set state and have the component re-render.

class UsersList extends Component {

    constructor(props){
        super(props)

        this.state = {
            users: props.users
        }

    }   

    filterByLastName = (lastName) => {
        const { users } = this.state

        const filtered = users.map(u => u.lastName === lastName)

        this.setState({ users: filtered })
    }


    render(){
        const { users } = this.state

        return(
            <>
                <button onClick={this.filterByLastName("Smith")}>Filter</button>
                <ul>
                    {
                        users.map(u => (
                            <>
                                <li>{u.firstName}</li>
                                <li>{u.lastName}</li>
                            </>
                        )
                    )}
                </ul>
            </>
        )
    }
}

The problem with doing this is the if the components props change i.e. this.props.users, it will cause a rerender but the constructor wont get called so the component will still have the old list of users assigned to it's props.users value. You could use componentDidUpdate (or another life cycle method) but this seems messy and over complicated.

If your data comes from props, it seems bad practice to assign it to state as you end up with 2 sources of truth.

What is the best way around this?

Upvotes: 1

Views: 184

Answers (2)

desko27
desko27

Reputation: 1736

The source of triggering the render is some sort of "active the filter" action, right? I would move filterByLastName logic to the beginning of the render method, then have a nameFilter var at state and make filterByLastName method set it instead.

This way you can check nameFilter state var to apply the filtering on the incoming prop at render, so render will only occur when filter is changed and you keep a single source of truth with no need to save the prop into state.

Solution on posted code

Not tested but I guess it could be something like this:

class UsersList extends Component {
    state = {
        nameFilter: null
    }

    filterByLastName = lastName => this.setState({ nameFilter: lastName })

    render(){
        const { users } = this.props
        const { nameFilter } = this.state

        const filtered = nameFilter ? users.filter(u => u.lastName === nameFilter) : users

        return(
            <>
                <button onClick={() => this.filterByLastName("Smith")}>Filter</button>
                <ul>
                    {
                        filtered.map(u => (
                            <>
                                <li>{u.firstName}</li>
                                <li>{u.lastName}</li>
                            </>
                        )
                    )}
                </ul>
            </>
        )
    }
}

Upvotes: 2

Estus Flask
Estus Flask

Reputation: 222354

This is what getDerivedStateFromProps is for. It calculates derived state on each component update.

It's necessary to keep all and filtered users separate because the list of filtered users needs to be reset on filter change.

const filterByLastName = (users, lastName) => users.map(u => u.lastName === lastName);

class UsersList extends Component {
    static getDerivedStateFromProps(props, state) {
      return {
        users: state.filter ? props.users : filterByLastName(props.users, state.filter)
      }
    }


    render(){
        const { users } = this.state

        return(
            <>
                <button onClick={() => this.setState({ filter: "Smith" })}>Filter</button>
                <ul>
                    {
                        users.map(u => (
                            <>
                                <li>{u.firstName}</li>
                                <li>{u.lastName}</li>
                            </>
                        )
                    )}
                </ul>
            </>
        )
    }
}

Upvotes: 2

Related Questions