Christoph Berger
Christoph Berger

Reputation: 371

Filtering an Array within an Array in React

import React, { Component } from "react"
import {
  StaticQuery,
  grahpql, 
  Link 
} from "gatsby"
import {
  StyledFilter,
  StyledLine
} from "./styled"

class Filter extends Component {

  render() {

    const { data } = this.props
    const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
      return (
        <a
          key={index}
          onClick={() => this.props.setFilterValue(cat.node.uid)}
        >
          {cat.node.data.category.text}
        </a>
      )
    })

    return (
      <StyledFilter>
        <div>
          Filter by<StyledLine />
          <a
            // onClick={() => {this.props.filterProjects("all")}}
          >
            All
          </a>
          {categories}
        </div>
        <a onClick={this.props.changeGridStyle}>{this.props.gridStyleText}</a>
      </StyledFilter>
    )
  }

}

export default props => (
  <StaticQuery
    query={graphql`
    query {
      allPrismicProjectCategory {
        edges {
          node {
            uid
            data {
              category {
                text
              }
            }
          }
        }
      }
    }
    `}
    render={data => <Filter data={data} {...props} />}
    />
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

I am working on a React App with Gatsby and Prismic that has a project page. By default it lists all projects but at the page's top appears a filter to select by category (just a bunch of <a> tags).

My Page consists of a <Filter /> component as well as several <GridItem /> components I am mapping over and load some props from the CMS.

The part I am struggling with is the filtering by category.

When my page component mounts it adds all projects into my filteredItems state.

When a user is clicking on a filter at the top it set's my default filterValue state from "all" to the according value. After that I'll first need to map over the array of projects and within that array I'll need to map over the categories (each project can belong to multiple categories).

My idea is basically if a value (the uid) matches my new this.state.filterValue it returns the object and add's it to my filteredItems state (and of course delete the one's not matching this criteria).

This is what my page component looks like (cleaned up for better readability, full code in the snippet at the bottom):

class WorkPage extends Component {

  constructor(props) {
    super(props)
    this.state = {
      filterValue: "all",
      filteredItems: []
    }
    this.filterProjects = this.filterProjects.bind(this)
  }

  filterProjects = (filterValue) => {
    this.setState({ filterValue: filterValue }, () => 
      console.log(this.state.filterValue)
    )
    // see a few of my approaches below
  }

  componentDidMount() {
    this.setState({
      filteredItems: this.props.data.prismicWork.data.projects
    })
  }

  render() {

    const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
      const item = node.project_item.document["0"].data
      const categories = node.project_item.document["0"].data.categories.map(cat => {
        return cat.category_tag.document["0"].uid
      })

      return (
        <GridItem
          key={index}
          categories={categories}
      moreContentProps={moreContentProps}
        />
      )
    })

    return (
      <LayoutDefault>
        <Filter
          filterProjects={this.filterProjects}
        />
    {projectItems}
      </LayoutDefault>

    )
  }

}

I tried so many things, I can't list all of them, but here are some examples:

This approach always returns an array of 10 objects (I have 10 projects), sometimes the one's that don't match the this.state.filterValue are empty objects, sometimes they still return their whole data.

let result = this.state.filteredItems.map(item => {
  return item.project_item.document["0"].data.categories.filter(cat => cat.category_tag.document["0"].uid === this.state.filterValue)
})
console.log(result)

After that I tried to filter directly on the parent item (if that makes sense) and make use of indexOf, but this always console logged an empty array...

let result = this.state.filteredItems.filter(item => {
  return (item.project_item.document["0"].data.categories.indexOf(this.state.filterValue) >= 0)
})
console.log(result)

Another approach was this (naive) way to map over first the projects and then the categories to find a matching value. This returns an array of undefined objects.

let result = this.state.filteredItems.map(item => {
  item = item.project_item.document["0"].data.categories.map(attachedCat => {
    if (attachedCat.category_tag.document["0"].uid === this.state.filterValue) {
      console.log(item)
    }
  })
})
console.log(result)

Other than that I am not even sure if my approach (having a filteredItems state that updates based on if a filter matches the according category) is a good or "right" React way.

Pretty stuck to be honest, any hints or help really appreciated.

import React, { Component } from "react"
import { graphql } from "gatsby"
import LayoutDefault from "../layouts/default"
import { ThemeProvider } from "styled-components"
import Hero from "../components/hero/index"
import GridWork from "../components/grid-work/index"
import GridItem from "../components/grid-item/index"
import Filter from "../components/filter/index"

class WorkPage extends Component {

  constructor(props) {
    super(props)
    this.state = {
      filterValue: "all",
      filteredItems: [],
      isOnWorkPage: true,
      showAsEqualGrid: false
    }
    this.filterProjects = this.filterProjects.bind(this)
    this.changeGridStyle = this.changeGridStyle.bind(this)
  }

  changeGridStyle = (showAsEqualGrid) => {
    this.setState(prevState => ({
      showAsEqualGrid: !prevState.showAsEqualGrid,
      isOnWorkPage: !prevState.isOnWorkPage
    }))
  }

  filterProjects = (filterValue) => {
    this.setState({ filterValue: filterValue }, () => 
    console.log(this.state.filterValue)
    )


    let result = this.state.filteredItems.filter(item => {
      return (item.project_item.document["0"].data.categories.toString().indexOf(this.state.filterValue) >= 0)
    })
    console.log(result)
  } 

  componentDidMount() {
    this.setState({
      filteredItems: this.props.data.prismicWork.data.projects
    })
  }

  render() {

    const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
      const item = node.project_item.document["0"].data

      const categories = node.project_item.document["0"].data.categories.map(cat => {
        return cat.category_tag.document["0"].uid
      })

      return (
        <GridItem
          key={index}
          isSelected="false"
          isOnWorkPage={this.state.isOnWorkPage}
          isEqualGrid={this.state.showAsEqualGrid}
          projectURL={`/work/${node.project_item.uid}`}
          client={item.client.text}
          tagline={item.teaser_tagline.text}
          categories={categories}
          imageURL={item.teaser_image.squarelarge.url}
          imageAlt={item.teaser_image.alt}
        />
      )
    })

    return (
      <ThemeProvider theme={{ mode: "light" }}>
        <LayoutDefault>
          <Hero
            introline="Projects"
            headline="Art direction results in strong brand narratives and compelling content."
          />
          {/* {filteredResult} */}
          <Filter
            filterProjects={this.filterProjects}
            changeGridStyle={this.changeGridStyle}
            gridStyleText={this.state.showAsEqualGrid ? "Show Flow" : "Show Grid"}
          />
          <GridWork>
            {projectItems}
          </GridWork>
        </LayoutDefault>
      </ThemeProvider>
    )
  }

}

export default WorkPage

export const workQuery = graphql`
  query Work {
    prismicWork {
      data {
        page_title {
          text
        }

        # All linked projects
        projects {
          project_item {
            uid

            # Linked Content
            document {
              type
              data {
                client {
                  text
                }
                teaser_tagline {
                  text
                }
                teaser_image {
                  url
                  alt
                  xlarge {
                    url
                  }
                  large {
                    url
                  }
                  medium {
                    url
                  }
                  squarelarge {
                    url
                  }
                  squaremedium {
                    url
                  }
                  squaresmall {
                    url
                  }
                }

                categories {
                  category_tag {
                    document {
                      uid
                      data {
                        category {
                          text
                        }
                      }
                    }
                  }
                }

              }
            }
          }
        }
      }
    }
  }
`
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Upvotes: 1

Views: 385

Answers (1)

Wu Wei
Wu Wei

Reputation: 2277

So there are at least two things.

  1. In your filterProjects() you're first setting state.filterValue and then you use it in filteredItems.filter(). That might not work, because React does not execute setState() immediately always, to optimize performance. So you're probably filtering against the previous value of state.filterValue. Instead just use filterValue, which you pass into filterProjects().
setFilterValue = (filterValue) => {
    this.setState({filterValue}) // if key and variable are named identically, you can just pass it into setState like that
}

// arrow function without curly braces returns without return statement
filterProjects = (projects, filterValue) =>
    projects.filter(item => item.project_item.document[0].data.categories.toString().includes(filterValue))
  1. You should return the result from filterProjects(), because you need to render based on the filteredItems then, of course. But actually it's not necessary to put the filter result into state. You can apply the filterProjects() on the props directly, right within the render(). That's why you should return them. Also separate setState into another function which you can pass into your <Filter/> component.

And a recommendation: Use destructuring to make your code more readable. For you and anyone else working with it.

render() {
    const { projects } = this.props.data.prismicWork.data // this is
    const { filterValue } = this.state                    // destructuring
    if (projects != undefined) {
        this.filterProjects(projects, filterValue).map((node, index) => {
        // ...

// Filter component
<Filter filterProjects={this.setFilterValue} />

That way you trigger a rerender by setting the filterValue, because it resides in this.state, and the render function depends on this.state.filterValue.

Please try that out and tell me if there is another problem.

Upvotes: 1

Related Questions