SelfTaughtLooking4Job
SelfTaughtLooking4Job

Reputation: 83

How to Filter by States Within React Components?

I am making a front-end UI returning student objects (grades, email, etc.) from an API call. I currently have a filter set up to return objects by names. I need to set up a second filter by tags, which can be added through an input element within each student component returned from .map() the API. I cannot figure out how to set up the filter as the tags are stored within each instance of the student Profile.js component. Can you please help me? Ultimately the UI should return search results from both filters (name && tags)

Snippet from App.js:

function App() {
 const [students, setStudents] = useState([])
 const [filteredStudents, setFilteredStudents] = useState([])
 const [search, setSearch] = useState("")
 

// Get request and store response in the 'students' state //

 useEffect(()=>{
    axios.get('Link to the API')
    .then(res => {
      const result = res.data.students
      setStudents(result)
    })
  },[])

// Filter students by name and store filtered result in 'filteredStudents' //

 useEffect(() => {
    const searchResult = []

    students.map(student => {
      const firstName = student.firstName.toLowerCase()
      const lastName = student.lastName.toLowerCase()
      const fullName = `${firstName}` + ` ${lastName}`

        if (fullName.includes(search.toLowerCase())) {
          searchResult.push(student)
        }
      return
    })

    setFilteredStudents(searchResult)
  }, [search])

return (
 <div>
    <SearchBar
      search={search}
      onChange={e => setSearch(e.target.value)}
    />

//Second search bar by tag here//
    
   {search.length == 0 &&
      //unfiltered students object here
   }

   {search.length != 0 &&
     <div>
        {filteredStudents.map(student => (
          <Profile
             //Some props here//
          />
        ))}   
     </div>
   }
 </div>
)}


Snippet from Profile.js

//bunch of code before this line//

    const [tags, setTags] = useState([])
    const [tag, setTag] = useState("")

    function handleKeyPress(e) {

        if(e.key === 'Enter') {
            tags.push(tag)
            setTag("")
        }
    }

return(
 <div>

//bunch of code before this line//

   <Tag
     onChange={e => setTag(e.target.value)}
     onKeyPress={handleKeyPress}
     tags={tags}
     tag={tag} 
   />
 </div>
)

Snippet from Tag.js:

export default function Tag({tag, tags, onChange, onKeyPress}) {

    return (
        <div>
            {tags.length > 0 &&
                <div>
                    {tags.map(tag => (
                        <span>{tag}</span>
                    ))}
                </div>
            }       
            <input 
                type='text'
                value={tag}
                placeholder="Add a tag"
                key='tag-input'
                onKeyPress={onKeyPress}
                onChange={onChange}
            />
        </div>
    )
}

Upvotes: 0

Views: 1176

Answers (1)

Sam McElligott
Sam McElligott

Reputation: 326

Edit

With your comment I think I now understand what you're trying to do, the images you provided really helped. You want to change the student object whenever a tag is added in the Profile component if I'm not mistaken (again, correct me if I am). That would mean the Profile component needs access to a handler so that whenever a tag is added, it also sets a new students state. It would look like this:

App.js

function App() {
  const [students, setStudents] = useState([]);
  const [filteredStudents, setFilteredStudents] = useState([]);

  const [search, setSearch] = useState("");

  const handleTagAdded = (tag, index) => {
    setStudents((prevStudents) => {
      // We copy object here as the student we're accessing
      // is an object, and objects are always stored by reference.
      // If we didn't do this, we would be directly mutating 
      // the student at the index, which is bad practice
      const changedStudent =  {...prevStudents[index]};

      // Check if student has 'tags` and add it if it doesn't.
      if (!("tags" in changedStudent)){
        changedStudent.tags = [];
      }

      // Add new tag to array
      changedStudent.tags.push(tag);
      
      // Copy array so we can change it
      const mutatableStudents = [...prevStudents];
      mutatableStudents[index] = changedStudent;

      // The state will be set to this array with the student
      // at the index we were given changed
      return mutatableStudents;
    })
  }

  // Get request and store response in the 'students' state //
  useEffect(() => {
    axios.get("Link to the API").then((res) => {
      const result = res.data.students;
      setStudents(result);
    });
  }, []);

  // Filter students by name and tag, then store filtered result in //'filteredStudents'
  useEffect(() => {
    // Array.filter() is perfect for this situation //
    const filteredStudentsByNameAndTag = students.filter((student) => {
      const firstName = student.firstName.toLowerCase();
      const lastName = student.lastName.toLowerCase();
      const fullName = firstName + lastName;

      if ("tags" in student){
        // You can now do whatever filtering you need to do based on tags
        ...
      }

      return fullName.includes(search.toLowerCase()) && yourTagComparison;
    });

    setFilteredStudents(filteredStudentsByNameAndTag);
  }, [search]);

  return (
    <div>
      <SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />

      //Second search bar by tag here //

      {search.length === 0 && 
        // unfiltered students //
      }

      {search.length !== 0 && (
        <div>
          {filteredStudents.map((student, index) => (
            <Profile
              // Some props here //
            
              onTagAdded={handleTagAdded}
              // We give the index so Profile adds to the right student
              studentIndex={index}
            />
          ))}
        </div>
      )}
    </div>
  );
}

In handleTagAdded, I copy the object at prevStudents[index] because it is a reference. This may sound odd if you don't know what I'm referring to (pun intended). Here is a link to an article explaining it better than I will be able to.

Profile.js

function Profile({ onTagAdded, studentIndex }) {
  // Other stuff //
  const [tags, setTags] = useState([]);
  const [tag, setTag] = useState("");

  const handleTagKeyPress = (e) => {
    if (e.key === "Enter") {
      // Use this instead of tags.push, when changing state you always
      // must use the `setState()` function. If the new value depends on the 
      // previous value, you can pass it a function which gets the 
      // previous value as an argument like below. It is also bad 
      // practice to change, or 'mutate' the argument you're given 
      // so we instead copy it and change that.

      setTags((previousTags) => [...previousTags].push(tag));
      setTag("");
      onTagAdded(tag, studentIndex)
    }
  };

  return (
    <div>
      // Other stuff
      <Tag onChange={(e) => setTag(e.target.value)} onKeyPress={handleTagKeyPress} tags={tags} tag={tag} />
    </div>
  );
}

Now, each <Profile /> component has its own tags state, but through the use of handleTagAdded(), we can change the student within each profile component based on tags.

Apologies for the confusion in my first answer, I hope this solves your issue!

Old answer

There's a very important concept in React known as "Lifting State". What this means is that if a parent component needs to access the state of a child component, one solution is to 'lift' the state from the child to the parent.

You can read some more about it in the React documentation.

In this example, you need to lift the tag state up from the <Profile /> component to the <App /> component. That way, both search and tag are in the same place and can be compared.

I believe the code below is along the lines of what you want:

App.js

function App() {
  const [students, setStudents] = useState([]);
  const [filteredStudents, setFilteredStudents] = useState([]);
  
  const [tags, setTags] = useState([]);
  const [tag, setTag] = useState("");

  const [search, setSearch] = useState("");

  const handleTagChange = (e) => setTag(e.target.value);

  const handleTagKeyPress = (e) => {
    if (e.key === "Enter") {
      // Use this instead of tags.push, when changing state you always
      // must use the `setState()` function. If the new value depends on the 
      // previous value, you can pass it a function which gets the 
      // previous value as an argument like below.
      setTags((previousTags) => previousTags.push(tag));
      setTag("");
    }
  };

  // Get request and store response in the 'students' state //
  useEffect(() => {
    axios.get("Link to the API").then((res) => {
      const result = res.data.students;
      setStudents(result);
    });
  }, []);

  // Filter students by name and tag, then store filtered result in //'filteredStudents'
  useEffect(() => {
    // Array.filter() is perfect for this situation //
    const filteredStudentsByNameAndTag = students.filter((student) => {
      const firstName = student.firstName.toLowerCase();
      const lastName = student.lastName.toLowerCase();
      const fullName = firstName + lastName;

      return fullName.includes(search.toLowerCase()) && student.tag === tag;
    });

    setFilteredStudents(filteredStudentsByNameAndTag);
  }, [search]);

  return (
    <div>
      <SearchBar search={search} onChange={(e) => setSearch(e.target.value)} />

      //Second search bar by tag here //

      {search.length == 0 && 
        // unfiltered students //
      }

      {search.length != 0 && (
        <div>
          {filteredStudents.map((student) => (
            <Profile
            // Some props here //
            onChange={handleTagChange}
            onKeyPress={handleTagKeyPress}
            tag={tag}
            tags={tags}
            />
          ))}
        </div>
      )}
    </div>
  );
}

Profile.js

function Profile({ onChange, onKeyPress, tags, tag }) {
  // Other stuff //

  return (
    <div>
      // Other stuff
      <Tag onChange={onChange} onKeyPress={onKeyPress} tags={tags} tag={tag} />
    </div>
  );
}

We've moved the tag state up to the <App /> component, so now when we filter we can use both the search and tag. I also changed students.map to students.filter as it is a better alternative for filtering an array.

I'm not clear on how you wanted to filter the tags, so I assumed the student object would have a tag attribute. Feel free to correct me about how the data is structured and I'll reformat it.

I hope this helped, let me know if you have any more problems.

Upvotes: 1

Related Questions