Search code examples
javascriptreactjsapireact-functional-componentreact-class-based-component

Converting class component to functional component TypeError: users.filter is not a function


Making a employee directory and I'm trying to convert class component into functional component. The nav, search, and table will render but the call to the api isn't working. Getting a TypeError: users.filter is not a function Here is the class component that works and the in progress functional component. I can't figure out what is different.

import React, { Component } from "react";
import DataTable from "./DataTable";
import Nav from "./Nav";
import API from "../utils/API";
import "../styles/DataArea.css";

export default class DataArea extends Component {
  state = {
    users: [{}],
    order: "descend",
    filteredUsers: [{}]
  }

  headings = [
    { name: "Image", width: "10%" },
    { name: "Name", width: "10%" },
    { name: "Phone", width: "20%" },
    { name: "Email", width: "20%" },
    { name: "DOB", width: "10%" }
  ]

  handleSort = heading => {
    if (this.state.order === "descend") {
      this.setState({
        order: "ascend"
      })
    } else {
      this.setState({
        order: "descend"
      })
    }

    const compareFnc = (a, b) => {
      if (this.state.order === "ascend") {
        // account for missing values
        if (a[heading] === undefined) {
          return 1;
        } else if (b[heading] === undefined) {
          return -1;
        }
        // numerically
        else if (heading === "name") {
          return a[heading].first.localeCompare(b[heading].first);
        } else {
          return a[heading] - b[heading];
        }
      } else {
        // account for missing values
        if (a[heading] === undefined) {
          return 1;
        } else if (b[heading] === undefined) {
          return -1;
        }
        // numerically
        else if (heading === "name") {
          return b[heading].first.localeCompare(a[heading].first);
        } else {
          return b[heading] - a[heading];
        }
      }

    }
    const sortedUsers = this.state.filteredUsers.sort(compareFnc);
    this.setState({ filteredUsers: sortedUsers });
  }

  handleSearchChange = event => {
    console.log(event.target.value);
    const filter = event.target.value;
    const filteredList = this.state.users.filter(item => {
      // merge data together, then see if user input is anywhere inside
      let values = Object.values(item)
        .join("")
        .toLowerCase();
      return values.indexOf(filter.toLowerCase()) !== -1;
    });
    this.setState({ filteredUsers: filteredList });
  }

  componentDidMount() {
    API.getUsers().then(results => {
      this.setState({
        users: results.data.results,
        filteredUsers: results.data.results
      });
    });
  }

  render() {
    return (
      <>
        <Nav handleSearchChange={this.handleSearchChange} />
        <div className="data-area">
          <DataTable
            headings={this.headings}
            users={this.state.filteredUsers}
            handleSort={this.handleSort}
          />
        </div>
      </>
    );
  }
}
import React, { useState, useEffect } from "react";
import DataTable from "./DataTable";
import Nav from "./Nav";
import API from "../utils/API";
import "../styles/DataArea.css";

const DataArea = () => {

  const [users, setUsers] = useState([{}]);
  const [order, setOrder] = useState("descend");
  const [filteredUsers, setFilteredUsers] = useState([{}]);


  const headings = [
    { name: "Image", width: "10%" },
    { name: "Name", width: "10%" },
    { name: "Phone", width: "20%" },
    { name: "Email", width: "20%" },
    { name: "DOB", width: "10%" },
  ];

  const handleSort = (heading) => {

    if (order === "descend") {
      setOrder((order = "ascend"));
    } else {
      setOrder((order = "descend"));
    }

    const compareFnc = (a, b) => {
      if (order === "ascend") {
        // account for missing values
        if (a[heading] === undefined) {
          return 1;
        } else if (b[heading] === undefined) {
          return -1;
        }
        // numerically
        else if (heading === "name") {
          return a[heading].first.localeCompare(b[heading].first);
        } else {
          return a[heading] - b[heading];
        }
      } else {
        // account for missing values
        if (a[heading] === undefined) {
          return 1;
        } else if (b[heading] === undefined) {
          return -1;
        }
        // numerically
        else if (heading === "name") {
          return b[heading].first.localeCompare(a[heading].first);
        } else {
          return b[heading] - a[heading];
        }
      }
    };

    const sortedUsers = filteredUsers.sort(compareFnc);
    setFilteredUsers({ filteredUsers: sortedUsers });
  };

  let handleSearchChange = (event) => {
    console.log(event.target.value);
    const filtered = event.target.value;
    const filteredList = users.filter((item) => {
      // merge data together, then see if user input is anywhere inside
      let values = Object.values(item).join("").toLowerCase();
      return values.indexOf(filtered.toLowerCase()) !== -1;
    });
    setFilteredUsers((filteredUsers = filteredList));
  };

  useEffect(() => {
    API.getUsers().then((results) => {
      setUsers({
        users: results.data.results,
        filteredUsers: results.data.results,
      });
    });

  }, []);
  return (
    <>
      <Nav handleSearchChange={handleSearchChange} />
      <div className="data-area">
        <DataTable
          headings={headings}
          users={filteredUsers}
          handleSort={handleSort}
        />
      </div>
    </>
  );
};

export default DataArea;

Solution

  • The issue is in your useEffect hook where you shoehorn both users and filteredUsers into your users state.

    useEffect(() => {
      API.getUsers().then((results) => {
        setUsers({
          users: results.data.results,
          filteredUsers: results.data.results,
        });
      });
    }, []);
    

    It should be

    useEffect(() => {
      API.getUsers().then((results) => {
        setUsers(results.data.results);
        setFilteredUsers(results.data.results),
      });
    }, []);
    

    You want to populate both the users state, and separately the filteredUsers state.

    An issue with setting the sorting order, you are mutating the order state:

    if (order === "descend") {
      setOrder((order = "ascend")); // <-- mutates
    } else {
      setOrder((order = "descend")); // <-- mutates
    }
    

    You can simply toggle between the two:

    setOrder(order => order === 'ascend' ? 'descend' : 'ascend');
    

    Another issue is at the end of your handleSort function:

    const sortedUsers = filteredUsers.sort(compareFnc);
    setFilteredUsers({ filteredUsers: sortedUsers });
    

    This will nest the filteredUsers array, and also mutates the array in-place, it should probably use a functional state update to update from the previous state, use array.slice to make a copy, and then call sort.

    setFilteredUsers(filteredUsers => filteredUsers.slice().sort(compareFnc));