Saladon
Saladon

Reputation: 85

Function Component always using previous state in render

I'm pretty new to using hooks and functional components.

I have a Filtered List. When I try to update the filter, it will use the last filter state instead of the new one. I must be missing some render/state change orders, but I can't seem to figure out what it is.

I appreciate any help I can get :)

Pseudo code below:

export default function TransferList(props) {
  const [wholeList, setWholeList] = React.useState([]);
  const [filteredList, setFilteredList] = React.useState([]);
  const [filter, setFilter] = React.useState([]);

  return (
    <>
      <TextField
        value={filter}
        onChange={(e) => {
          // Set filter input
          setFilter(e.target.value);

          // Filter the list
          let filtered = wholeList.filter(
            (item) => item.indexOf(filter) !== -1
          );
          setFilteredList(filtered);
        }}
      />

      <List>
        {filteredList.map((item) => (
          <ListItem>Item: {item}</ListItem>
        ))}
      </List>
    </>
  );
}

Upvotes: 0

Views: 502

Answers (4)

Omer
Omer

Reputation: 3466

Order your code to be increase readability

In clean code

Main changes:

  • Use destructuring instead of props
  • Out the jsx from the html return to increase readability
  • Use includes instead of indexOf
  • Add key to the list

    export default function TransferList({ wholeList }) {
    
      const [filteredList, setFilteredList] = React.useState(wholeList);
      const [filter, setFilter] = React.useState([]);
    
      const handleOnChange = ({ target }) => {
        setFilter(target.value);
        const updatedFilteredList = wholeList.filter(item => item.includes(target.value));
        setFilteredList(updatedFilteredList);
      }
    
      const list = filteredList.map(item => {
        return <ListItem key={item}>Item: {item}</ListItem>
      });
    
      return (
        <>
          <TextField value={filter} onChange={handleOnChange} />
          <List>{list}</List>
        </>
      );
    }
    

and I have a filter component the do the filter list inside.

import React, { useState } from 'react';

function FilterInput({ list = [], filterKeys = [], placeholder = 'Search',
  onFilter }) {

  const [filterValue, setFilterValue] = useState('');

  const updateFilterValue = ev => {
    setFilterValue(ev.target.value);
    const value = ev.target.value.toLowerCase();
    if (!value) onFilter(list);
    else {
      const filteredList = list.filter(item => filterKeys.some(key => item[key].toLowerCase().includes(value)));
      onFilter(filteredList);
    }
  }

  return (
    <input className="filter-input" type="text" placeholder={placeholder}
      value={filterValue} onChange={updateFilterValue} />
  );
}

export default FilterInput;

the call from the father component look like this

<FilterInput list={countries} filterKeys={['name']} placeholder="Search Country"
          onFilter={filterCountries} />

this is in my corona app.. you can look on my Github.

and see how I build the filter

(https://github.com/omergal99/hello-corona)

Upvotes: 0

Saladon
Saladon

Reputation: 85

So it turns out I had to give the initial filtered list the entire unfiltered list first. For some reason that fixes it.

const [filteredList, setFilteredList] = React.useState(props.wholeList);

I initially wanted an empty filter to display nothing, but I may have to settle for showing the entire list when the filter is empty

Upvotes: 0

Shubham Khatri
Shubham Khatri

Reputation: 281626

State updates are asynchronous and hence the filter state update doesn't reflect immediately afterwords

You must store the new filter values and set the states based on that

export default function TransferList(props) {

    const [wholeList, setWholeList] = React.useState([]);
    const [filteredList, setFilteredList] = React.useState([]);
    const [filter, setFilter] = React.useState([]);

    return (
        <>
            <TextField value={filter} onChange={(e) => {
                // Set filter input
                const newFilter = e.target.value;
                setFilter(newFilter)

                // Filter the list
                let filtered = wholeList.filter(item => item.indexOf(newFilter) !== -1)
                setFilteredList(filtered)
            }} />

            <List>
                {filteredList.map(item => <ListItem>Item: {item}</ListItem>)}
            </List>
        </>
    )
}

Upvotes: 1

Jagrati
Jagrati

Reputation: 12222

Inside onChange you should use save the value in a constant, filter will not update just after setFilter(filterValue) as this is an async operation.

<TextField
  value={filter}
  onChange={e => {
    const filterValue = e.target.value;
    // Set filter input
    setFilter(filterValue);

    // Filter the list
    let filtered = wholeList.filter(item => item.indexOf(filterValue) !== -1);
    setFilteredList(filtered);
  }}
/>;

Upvotes: 1

Related Questions