React: Javascript filter search not working on backspace

Viewed 928

I'm trying to do a simple filter based on input text, but it has a bug, when i press 'backspace', it should refilter based on the original arrays, not the previous filtered array. I know what the problem is: I changed the original tasks every time when it filters. And i know i should do something like making a copy of the original arrays and filter based on the copy. But i just don't know how to achieve that in react.

Below is my code:

export class Main extends Component {
    constructor(pros) {
        super(pros)
        this.state = {
            tasks: [
                {
                    id: 1,
                    content: "Sara's doctor and vaccine",
                    due: '2020-08-29',
                    completed: false
                },
                {
                    id: 2,
                    content: "Trash bags / facial masks / child allowance",
                    due: '2020-08-28',
                    completed: false
                },
                {
                    id: 3,
                    content: "Apply for Portugal nationality",
                    due: '2020-09-31',
                    completed: false
                },
                {
                    id: 4,
                    content: "My registration card",
                    due: '2020-09-28',
                    completed: false
                }
            ]
        }


    handleSearch = (e) => {
        let searchValue = e.target.value
        console.log(searchValue)
        let filteredTasks = this.state.tasks.filter(task => {
            return task.content.toLowerCase().includes(searchValue.toLowerCase())
        })
        this.setState(state => ({
            tasks: filteredTasks
        }))
    }


    render() {
        return (
            <div>
                <div style={{ textAlign: 'right' }}><input type='search' onKeyUp={this.handleSearch} id='search' name='search' placeholder='Search Tasks' autoComplete='on' style={{ width: '60%', margin: '15px 15px 45px auto' }} /></div>

                <table>
                    <caption>Good {this.state.dayPeriod}! &hearts; {this.state.userName}</caption>
                    <thead>
                        <tr>
                            <th>
                                <button type='button' onClick={this.handleSelect}>Select All</button>
                            </th>
                            <th>ID</th>
                            <th>Content</th>
                            {/* <th>Created On</th> */}
                            <th>Due</th>
                            <th>Completed</th>
                        </tr>
                    </thead>
                    <tbody>
                        {this.state.tasks.reverse().map((el, i) => (
                            <tr key={i} className='row' style={{ textDecoration: el.completed ? this.state.textDecoration : 'none', color: el.completed ? this.state.color : '#000000' }}>
                                <td>
                                    <input type='checkbox' checked={el.completed} onChange={() => { el.completed = !el.completed }}></input>
                                </td>
                                <td className='taskID' style={{ verticalAlign: 'text-top' }}>{el.id}</td>
                                <td className='taskContent'>{el.content}</td>
                                {/* <td style={{whiteSpace: 'nowrap'}}>{new Date().getFullYear()}-{new Date().getMonth().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}-{new Date().getDate().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}</td> */}
                                <td style={{ whiteSpace: 'nowrap' }}>{el.due}</td>
                                <td>{el.completed === false ? 'N' : 'Y'}</td>
                            </tr>
                        ))}

                        {/* {this.listTasks()} */}
                    </tbody>
                </table>
            </div>
        )
    }
}
4 Answers

There are many ways to achieve what you are asking, but simplest way is to not change the state, only change what should be rendered. On top of your code I've added the filter, so the state remains the same but only the filter applies on the results:

export class Main extends Component {
    constructor(pros) {
        super(pros)
        this.state = {
            tasks: [
                {
                    id: 1,
                    content: "Sara's doctor and vaccine",
                    due: '2020-08-29',
                    completed: false
                },
                {
                    id: 2,
                    content: "Trash bags / facial masks / child allowance",
                    due: '2020-08-28',
                    completed: false
                },
                {
                    id: 3,
                    content: "Apply for Portugal nationality",
                    due: '2020-09-31',
                    completed: false
                },
                {
                    id: 4,
                    content: "My registration card",
                    due: '2020-09-28',
                    completed: false
                }
            ],
            searchValue: ""
        }


    handleSearch = (e) => {
        this.setState({ searchValue: e.target.value })
    }

    filterResults = () => {
        if(!this.state.searchValue) return this.state.tasks
        return this.state.tasks.filter(task => {
            return task.content.toLowerCase().includes(this.state.searchValue.toLowerCase())
        })
    }


    render() {
        return (
            <div>
                <div style={{ textAlign: 'right' }}><input type='search' onKeyUp={this.handleSearch} id='search' name='search' placeholder='Search Tasks' autoComplete='on' style={{ width: '60%', margin: '15px 15px 45px auto' }} /></div>

                <table>
                    <caption>Good {this.state.dayPeriod}! &hearts; {this.state.userName}</caption>
                    <thead>
                        <tr>
                            <th>
                                <button type='button' onClick={this.handleSelect}>Select All</button>
                            </th>
                            <th>ID</th>
                            <th>Content</th>
                            {/* <th>Created On</th> */}
                            <th>Due</th>
                            <th>Completed</th>
                        </tr>
                    </thead>
                    <tbody>
                        {filterResults().reverse().map((el, i) => (
                            <tr key={i} className='row' style={{ textDecoration: el.completed ? this.state.textDecoration : 'none', color: el.completed ? this.state.color : '#000000' }}>
                                <td>
                                    <input type='checkbox' checked={el.completed} onChange={() => { el.completed = !el.completed }}></input>
                                </td>
                                <td className='taskID' style={{ verticalAlign: 'text-top' }}>{el.id}</td>
                                <td className='taskContent'>{el.content}</td>
                                {/* <td style={{whiteSpace: 'nowrap'}}>{new Date().getFullYear()}-{new Date().getMonth().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}-{new Date().getDate().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}</td> */}
                                <td style={{ whiteSpace: 'nowrap' }}>{el.due}</td>
                                <td>{el.completed === false ? 'N' : 'Y'}</td>
                            </tr>
                        ))}

                        {/* {this.listTasks()} */}
                    </tbody>
                </table>
            </div>
        )
    }
}

You should extract original Task List out of state and always filter on that. That way you will have reference of all Tasks every time you filter. Something like this.

const allTasks = [
    {
        id: 1,
        content: "Sara's doctor and vaccine",
        due: '2020-08-29',
        completed: false
    },
    {
        id: 2,
        content: "Trash bags / facial masks / child allowance",
        due: '2020-08-28',
        completed: false
    },
    {
        id: 3,
        content: "Apply for Portugal nationality",
        due: '2020-09-31',
        completed: false
    },
    {
        id: 4,
        content: "My registration card",
        due: '2020-09-28',
        completed: false
    }
];


export class Main extends Component {
    constructor(pros) {
        super(pros)
        this.state = {
            tasks: allTasks
        }
    }

    handleSearch = (e) => {
        let searchValue = e.target.value
        console.log(searchValue)
        let filteredTasks = allTasks.filter(task => {
            return task.content.toLowerCase().includes(searchValue.toLowerCase())
        })
        this.setState(state => ({
            tasks: filteredTasks
        }))
    }


    render() {
        return (
            <div>
                <div style={{ textAlign: 'right' }}><input type='search' onKeyUp={this.handleSearch} id='search' name='search' placeholder='Search Tasks' autoComplete='on' style={{ width: '60%', margin: '15px 15px 45px auto' }} /></div>

                <table>
                    <caption>Good {this.state.dayPeriod}! &hearts; {this.state.userName}</caption>
                    <thead>
                        <tr>
                            <th>
                                <button type='button' onClick={this.handleSelect}>Select All</button>
                            </th>
                            <th>ID</th>
                            <th>Content</th>
                            {/* <th>Created On</th> */}
                            <th>Due</th>
                            <th>Completed</th>
                        </tr>
                    </thead>
                    <tbody>
                        {this.state.tasks.reverse().map((el, i) => (
                            <tr key={i} className='row' style={{ textDecoration: el.completed ? this.state.textDecoration : 'none', color: el.completed ? this.state.color : '#000000' }}>
                                <td>
                                    <input type='checkbox' checked={el.completed} onChange={() => { el.completed = !el.completed }}></input>
                                </td>
                                <td className='taskID' style={{ verticalAlign: 'text-top' }}>{el.id}</td>
                                <td className='taskContent'>{el.content}</td>
                                {/* <td style={{whiteSpace: 'nowrap'}}>{new Date().getFullYear()}-{new Date().getMonth().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}-{new Date().getDate().toLocaleString('en-US', {minimumIntegerDigits: 2, useGrouping:false})}</td> */}
                                <td style={{ whiteSpace: 'nowrap' }}>{el.due}</td>
                                <td>{el.completed === false ? 'N' : 'Y'}</td>
                            </tr>
                        ))}

                        {/* {this.listTasks()} */}
                    </tbody>
                </table>
            </div>
        )
    }
}

It's better to change the handleSearch function to:

 handleSearch = (e) => {
       setState({searchValue: e.target.value})
    }

and handle the filter in your map:

this.state.tasks.reverse()
.filter(task => task.content.toLowerCase().includes(searchValue.toLowerCase()))
.map(...

You are overriding the source data. You should keep an variable that store the source data

const TASK = [
  {
    id: 1,
    content: "Sara's doctor and vaccine",
    due: "2020-08-29",
    completed: false
  },
  {
    id: 2,
    content: "Trash bags / facial masks / child allowance",
    due: "2020-08-28",
    completed: false
  },
  {
    id: 3,
    content: "Apply for Portugal nationality",
    due: "2020-09-31",
    completed: false
  },
  {
    id: 4,
    content: "My registration card",
    due: "2020-09-28",
    completed: false
  }
];

// ...

  constructor(pros) {
    super(pros);
    this.state = {
      tasks: [...TASK]
    };
  }

  handleSearch = (e) => {
    let searchValue = e.target.value;
    console.log(searchValue);
    let filteredTasks = TASK.filter((task) => {
      return task.content.toLowerCase().includes(searchValue.toLowerCase());
    });
    this.setState((state) => ({
      tasks: filteredTasks
    }));
  };

// ...

Codesandbox demo

Edit compassionate-tdd-mgpdr

Related