Skip to content

Instantly share code, notes, and snippets.

@rogeliog
Created December 6, 2016 19:15
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save rogeliog/9455a6ad460361aa9a68fbe7657c4ee0 to your computer and use it in GitHub Desktop.
Save rogeliog/9455a6ad460361aa9a68fbe7657c4ee0 to your computer and use it in GitHub Desktop.
Composition and filters in React

I've been exploring how to implement filters in React, this is my approach to it.

movies

What is this trying to solve?

  • Provide a defined interface for declaring filters and getting state of them at any given time.
  • It should work with any UI.
  • It doesn't know about the data source(API, local, etc).
  • Leave the responsability of applying the filters to the consumer, our Filters component only takes care of keeping up to date each of the filters
  • Only the consumer knows what each filter "means".
<Filters onChange={this.onFilterChange}>
  <div>
    <span>Title</span>
    <InputFilter filterName="title" />
  </div>
  <SelectFilter filterName="year" />
</Filters>

<Filters />

It injects an updateFilter: func(name, value) function through the context to the 'filter' components.

props:

  • onChange: func({ filters }): It is called every time a filter changes and it receives the state of the filters as a param. { filters: { title: 'Frozen', year: '2013' } }

<InputFilter />

For this example I created an InputFilter, but you could imagine the same pattern working with a SelectFilter, RadioFilter, etc. It calls updateFilter everytime the value of the input changes.

NOTE: We could add a debounce when the user types insice the InputFilter and no other component or interface should be affected.

filter

A higher-order component that injects the updateFilter prop to a component and partially applies it so that the composed component doesn't have to care about the name of the filter.

What else can we do with these filters?

Nested filters

We could modify this to work with nested filters, which means that we could reuse a whole set of filters.

const PageFilters = () => (
  <Filters>
    <NumberFilter filterName={pageSize} />
  </Filters>
);

// Then we can reuse this...

<Filters onChange={this.onFilterChange}>
  <div>
    <span>Title</span>
    <InputFilter filterName="title" />
  </div>
  <PageFilters filterName="page" />
</Filters>

//Whe onChange gets called it will receive something like `{ filters: { title: 'The' }, page: { pageSize: 20 } }`

Child as func filter

<Filter filterName="hasClicked">
  {(updateFilter) => (
    <div onClick={() => updateFilter(true)}>Click me</div>
  )}
</Filter>
/*
This is a minimal working example
*/
import React, { Component } from 'react';
import Filters from './Filters';
import InputFilter from './InputFilter';
import allMovies from './movies';
class App extends Component {
state = {
filters: { title: '' },
}
onFilterChange = ({ filters }) => {
this.setState({ filters });
}
render() {
const { filters } = this.state;
const movies = allMovies.filter(({ title }) => (
title.includes(filters.title)
))
return (
<div>
<Filters onChange={this.onFilterChange}>
<InputFilter filterName="title" />
</Filters>
<ul>
{movies.map(({ title }) => <li key={title}>{title}</li>)}
</ul>
</div>
);
}
}
export default App;
import React, { Component, PropTypes } from 'react';
const { func, string } = PropTypes;
export default function filter(ComposedComponent) {
return class Filter extends Component {
static contextTypes = {
updateFilter: func.isRequired,
}
static propTypes = {
filterName: string.isRequired,
}
updateFilter = (value) => {
this.context.updateFilter(this.props.filterName, value);
}
render() {
const props = { ...this.props, updateFilter: this.updateFilter };
return <ComposedComponent {...props} />;
}
}
}
import React, { Component, PropTypes } from 'react';
const { func } = PropTypes;
class Filters extends Component {
static propTypes = {
onChange: func.isRequired,
}
static childContextTypes = {
updateFilter: func,
}
state = { filters: {} };
notifyChange = () => {
this.props.onChange(this.state.filters);
}
updateFilter = (name, value) => {
this.setState({
filters: { ...this.state.filters, [name]: value },
}, this.notifyChange);
}
getChildContext() {
return {
updateFilter: this.updateFilter,
}
}
render() {
return <div>{this.props.children}</div>;
}
}
export default Filters;
import React, { Component, PropTypes } from 'react';
import filter from './filter';
const { func, string } = PropTypes;
class InputFilters extends Component {
static propTypes = {
updateFilter: func.isRequired,
}
onChange = ({ target: { value } }) => {
this.props.updateFilter(value);
}
render() {
return <input onChange={this.onChange} />
}
}
export default filter(InputFilters);
@jserrao
Copy link

jserrao commented Dec 21, 2018

@rogeliog Interesting approach. I wonder how you'd update it with Context. I might post a response when I'm done building something similar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment