ReactJS - How to compare/filter matching id's of two arrays and use the result in a double search feature?

Viewed 918

I want to have a double search feature in my app. One search, filters books out by its title. While the other, filters books by genre. Both search filters should work together (it should filter out by title AND genre if search terms are provided).

I'm having difficulty applying the genre search filter, however.

** NOTE**: When created, each genre tag is assigned the same id as the book it's tagged to. This way, I can pull up all the books associated with a genre that's being search.

Objects:

book = { id: "id", title: "title", author: "author"}
genre = { id: "id", label: "label" }

Parent component

export default function Books() {
  const [books, setBooks] = useState([])
  const [searchTitle, setSearchTitle] = useState("")
  const [genres, setGenres] = useState([]) 
  const [searchGenre, setSearchGenre] = useState("")

  useEffect(() => {
    fetch(`https://www.someapi.com/books`)
    .then(r => r.json())
    .then(setBooks)
  }, [])

  function handleAllGenres(val){
    setGenres([...genres, val])
  }

  const filterGenres = genres.filter(genre => {
    return genre.label.toLowerCase().includes(searchGenre.toLowerCase())
  })

  const filterBooks = books.filter(book => {
    return book.title.toLowerCase().includes(searchTitle.toLowerCase())
  })

  const filterBookGenres = filterBooks.filter(book => {    
    if (searchGenres.length > 0) {                 
      return filterGenres.some(genre => book.id === genre.id) 
    } else {
      return books
    }
  })

  const book = filterBookGenres.map(book => {
    return (
        <Book 
            key={book.id} 
            book={book}
            genres={genres}
            onAddGenre={handleAllGenres}
        />
    )
  })

  return (
    <div className="main-container">
      <SearchTitle 
        searchTitle={searchTitle} 
        onSearchTitle={setSearchTitle} 
      />
      <SearchGenre 
        searchGenre={searchGenre}
        onSearchGenre={setSearchGenre}
      />
      {book}
    </div>
  );
}

Child Component:

export default function Book({ book, onAddGenre }) 
    const [genreTags, setGenreTags] = useState([]) 

    function handleAddGenre(e) {
        const val = {
            id: book.id,
            label: e.target.value,
        }

        if (e.key === 'Enter' && val) {
            if (genreTags.find(genre => genre.toLowerCase() === val.toLowerCase())) {
              return
            }
            setGenreTags([...genreTags, val])
            onAddGenre(val)
            e.target.value = ""
        }
    }

    return (
        <div className="book-div">
            <p>{title}</p>
            <p>Written By: {author}</p>
        </div>
        <div className="tag-div">
            <ul> 
                { genreTags && genreTags.map(tag => {
                    return <li key={uuid()}> {tag.label} </li>  
                })}
            </ul>
        </div>
        <input 
            type="text" 
            placeholder="Add a genre"
            onKeyDown={handleAddGenre} 
        />
    );
}

Search Components:

export default function SearchTitle({ searchTitle, onSearchTitle }) {

    return (
        <div className="search">
            <input
                id="name-input" 
                placeholder="Search by title"
                value={search} 
                onChange={e => onSearch(e.target.value)} 
            />
        </div>
    );
}
export default function SearchGenre({ searchGenre, onSearchGenre }) {
    return (
        <div className="search">
            <input 
                id="tag-input"
                placeholder="Search by genre"
                value={searchGenre}
                onChange={e => onSearchTag(e.target.value)} 
            />
        </div>
    )
}

UPDATE: The search features work now, but I noticed that my tags disappear after I play around with either search features. For example, if I input a search value and filter through the books list then delete my search terms, my tags will disappear. I assume it's because the books get re-rendered?

Is there a way to prevent this re-render or to prevent my tags from disappearing after playing around with my search feature?

All the help appreciated. Thank you!

2 Answers

It's hard to tell, since I can't see what's going on inside of the onClick handlers (they're in different components). I see one possible issue right off the bat (which is weird, considering you said genre was the issue and not title search). In filterBooks you have book => { return title.toLowerCase()..., but it should be book => { return book.title.toLowerCase()...

Also, in this code sample you're not setting setGenres. Is the genres state empty?

Edit

Is there a way to prevent this re-render or to prevent my tags from disappear after playing around with my search feature?

Yes, there's a workaround solution that we can use until we refresh the page or unmount and re-mount the Books component. Objects are mutable data structures, so you could mutate them to add a new key ("tags"). These will persist, even if the Book object dismounts and re-mounts. We can do this from handleAddGenre in Book.js like so:

  function handleAddGenre(e) {
    const val = {
      id: book.id,
      label: e.target.value
    };

    if (e.key === "Enter" && val) {
      if (
        /**
         * Note that I made a small change here, 
         * adding 'labels' before the two toLowerCase methods
         */
        genreTags.find(
          (genre) => genre.label.toLowerCase() === val.label.toLowerCase()
        )
      ) {
        return;
      }
      /**
       * we mutate the book object here.  
       * This will persist until we reset the books state,
       * so searching doesn't make it disappear.
       */
      book.tags = book.tags ? [...book.tags, val] : [val];
      setGenreTags([...genreTags, val]);
      onAddGenre(val);
      e.target.value = "";
    }
  }

Then, we just have to change the render accordingly, since it should iterate through the books' new tag key to find all of the new labels

<ul>
  {book.tags &&
    book.tags.map((tag) => {
      return <li key={uuid()}> {tag.label} </li>;
  })}
</ul>

Take a look at this.

codesandbox

import { useState } from "react";
import { Button, makeStyles } from "@material-ui/core";
import "./styles.css";

const genreList = [
  {
    id: 0,
    label: "horror",
    type: "fiction",
    desc: "these creepy crawler stories will leave you shaking in your boots!"
  },
  {
    id: 1,
    label: "action",
    type: "fiction",
    desc: "a fun, colorful adventure awaits!"
  },
  {
    id: 2,
    label: "history",
    type: "non-fiction",
    desc: "A blast from the past!"
  }
];

const bookList = [
  {
    id: 0,
    title: "My First Book",
    author: "Mint",
    genreId: 2
  },
  {
    id: 1,
    title: "My Second Book",
    author: "James Bond",
    genreId: 1
  },
  {
    id: 2,
    title: "Here's the Third!",
    author: "Batman",
    genreId: 1
  },
  {
    id: 3,
    title: "The Holy Fourth Empire",
    author: "John Snow",
    genreId: 0
  },
  {
    id: 4,
    title: "Remember Remember the Fifth of November!",
    autorh: "V",
    genreId: 0
  }
];

const useStyles = makeStyles({
  root: {
    flex: 1,
    display: "flex",
    flexDirection: "column"
  },
  header: {
    paddingBottom: "10px"
  },
  bookList: {
    height: "100px"
  }
});

export default function App() {
  const classes = useStyles();

  const [genres, setGenres] = useState(genreList);
  const [books, setBooks] = useState(bookList);
  const [isSelected, setIsSelected] = useState(null);

  return (
    <div className={classes.root}>
      <div className={classes.header}>
        {isSelected !== null ? (
          <h4>{genres[isSelected].label} Movies</h4>
        ) : (
          <h4>All Movies</h4>
        )}
      </div>
      <div className={classes.bookList}>
        {isSelected === null
          ? books.map((book) => <p>{book.title}</p>)
          : books
              .filter((book) => book.genreId === isSelected)
              .map((book) => <p>{book.title}</p>)}
      </div>
      <div className={classes.buttons}>
        <Button onClick={() => setIsSelected(0)}>Horror</Button>
        <Button onClick={() => setIsSelected(1)}>Action</Button>
        <Button onClick={() => setIsSelected(2)}>History</Button>
        <Button onClick={() => setIsSelected(null)}>All</Button>
      </div>
    </div>
  );
}
Related