Code-Along: Implementing 'Search By Criteria' Functionality with React, Formik, and Flask-SQLAlchemy
Tags: javascript, react, tutorial, python
Published: 2023-04-12 20:44:29

## My Current Project
As I reach the end of my journey as a bootcamp student, for my capstone project, I'm building an app that will allow users to search for national parks with particular accessibility-related amenities available. For today's blog, I wanted to walk through how I implemented a central piece of functionality for the app: the search. More specifically, the ability to search using multiple criteria.

### The Problem I'm Trying to Solve

The search was at the heart of why I had the idea to create this app in the first place.  In the official [National Park Service mobile app](https://www.nps.gov/subjects/digital/nps-apps.htm), a user can search for parks to explore based on:

- their proximity to the park
- the name of the park
- the state the park is located in
- activities available at the park
- topics discussed at the park
- the type of park (e.g., park, monument, recreation area)

![Screenshot from the official National Park Service mobile app showing the various filters available](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/op03gowuldzbivx4dweq.jpeg)

That's a lot of search options! However, despite there being a wide range of accessible offerings at the parks, there currently isn't an easy way for users to find parks based on the accessibility offerings available. A user has to click into each park, navigate to the amenities section, and from there, find the relevant section on accessibility.

The National Park Service website provides a [map of accessibility in the national parks](https://www.nps.gov/subjects/accessibility/plan-your-visit.htm), showing the location of all parks in the United States with accessible features and services. However, clicking on each location only provides a link to the park's accessibility page. There's no way to see at a glance which amenities are available. 

![Screenshot of the 'Accessibility' map on the official National Parks website, showing a universal wheelchair icon at each park location with accessible features, and a tooltip directing users to the corresponding park's accessibility page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfh3h269u975g74ghdka.png)

There's no way to know whether "accessibility" at a given park means having wheelchair-accessible trails, having a sign-language interpreter for guided tours, or having tactile exhibits for those with visual impairments. The user has to manually search each page to whittle down which ones have the amenities they need to make the most of their trip, which is needlessly time-consuming. 

### My Vision for the Search Function
When I first sketched out what I wanted the user interface to look like, I knew I wanted the homepage to be a list of the various accessibility and accessibility-adjacent (e.g., cellular signal, access to groceries, access to drinking water) amenities available at the parks. I wanted users to be able to select (by clicking the corresponding checkbox) any of the criteria that would help them decide where to go, click 'Search,' and have a list of parks returned to them matching all the criteria they selected.

![My initial sketch of the search page design, with a header at the top, three columns of checkboxes, and a search button at the bottom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z01yagsfnu13bcbrxzpp.png)

Let's code along together to make that vision a reality!

### Create 'Search' and 'Results' Components
We'll start by creating two components to house the search and results, respectively, and importing those into our main app to add them to the component tree. At the same time, let's add some basic routing by importing [`Switch`](https://v5.reactrouter.com/web/api/Switch) and [`Route`](https://v5.reactrouter.com/web/api/Route) from [React Router](https://v5.reactrouter.com/web/guides/quick-start). *(Note: I'm using version 5 of React Router here.)*

```javascript
// App.js

// React Imports
import { React } from 'react';
import { Switch, Route } from 'react-router-dom';

// Component Imports
// Note: This import reference will vary based on your file structure. In this case, the main App is stored outside the 'Components' folder.
import { Search } from './Components/Search';
import { Results } from './Components/Results';

function App() {

	return (
	<Switch>
		<Route path='/search'>
			<Search />
		</Route>
		<Route path='/results'>
			<Results />
		</Route>
	</Switch>
	)
};

export default App;
```

### Create the Initial Form
Next, in the search component, let's import React's [`useState`](https://react.dev/reference/react/useState) and [`useEffect`](https://react.dev/reference/react/useEffect) hooks. We'll put those to work right away, fetching the amenity data from the server from within `useEffect` with an empty dependency array so that the `GET` request only gets sent when the component initially renders, not continuously. Once that data is successfully retrieved, we'll save this array of amenity objects as a state variable.

With the amenities array saved in state, we can then iterate through using the [`map()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) method. Cycling through each amenity object in the array, we'll map its properties onto the attributes of an `<input>` element. The end result of the map will be a new array of checkbox elements that can then be added within the overall `<form>` being returned by the `Search` component.

Here's what the user interface should look like so far (without adding styling, for simplicity):

![Screenshot of basic search page, a single column list of checkboxes for all amenities available](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pw7jlregs05unyywt8zi.png)

Here's what the code should look like so far:

```javascript
// Search.js

// Importing React hooks
import { useState, useEffect } from 'react';

function Search() {

	// Setting state for amenities
	const [amenities, setAmenities] = useState([])

	// Fetching amenities from within useEffect
	useEffect( () => {
		fetch('/amenities')
			.then(response => response.json())
			.then(amenityData => setAmenities(amenityData))
	}, [])

	// Iterating through amenities to create an array 'checkbox' input elements.
	const checkboxes = amenities.map( (amenity) => {
		return (
			<div key={amenity.id}>
			<label>
				<input
					type="checkbox"
					name="checked"
					value={amenity.id}
				/>
				{amenity.name}
			</label>
			<br />
			</div>
		)
	})
	
	return (
		<div>
			<h1>Search</h1>
			<form>
				// Inserting that array of checkboxes into the form.
				{checkboxes}
				<input type="submit" value="Search" />
			</form>
		</div>
		)
	};

export default Search;
```

One thing that you'll notice looking at the code above is that each of the checkbox elements has the same name, "checked". This is by design, as we'll cover in the next section.

### Configure Formik
[Formik](https://formik.org/) is an incredibly useful React library for managing state within our forms. Setting up the [`useFormik`](https://formik.org/docs/api/useFormik) hook and using its built-in state and change handlers takes out a lot of the work that comes with setting up form fields as [controlled components](https://legacy.reactjs.org/docs/forms.html#controlled-components).

```javascript
// Search.js

// Other React imports

// Importing Formik
import { useFormik } from 'formik';

function Search() {

	// State-setting, useEffect fetching, etc.
	
	// Initializing the useFormik hook
	const formik = useFormik({
		initialValues: {
			checked: []
		},
		onSubmit: (values) => {
			console.log(values)
		}
	})
	
	// checkboxes, return, etc.
};
```
In the snippet of code above, we're initializing the `useFormik` hook. We're only specifiying one field here, 'checked' — corresponding with the `name` attribute that we assigned to each of the checkbox elements earlier — and setting it equal to an empty array for its initial value. Per [Formik's documentation on forms with checkbox groups](https://formik.org/docs/examples/checkboxes), "given that the fields all share the same `name`, Formik will automagically bind them to a single array". Anytime we click on a checkbox, that checkbox's value (which we've set to the amenity's unique ID) gets added to that array. If we click on the same box again to uncheck it, that value gets removed from the array.

Of course, to accomplish that, let's not forget that Formik's handlers need to get added on to the form elements. The `formik.handleChange` prop should get passed in to each input's `onChange` attribute:

```javascript
// Search.js

// imports

function Search() {

	// State-setting, useEffect fetching, Formik set-up, etc.
	
	const checkboxes = amenities.map( (amenity) => {
		return (
			<div key={amenity.id}>
				<label>
					<input
						type="checkbox"
						name="checked"
						value={amenity.id}
						// Adding Formik's built-in onChange handler.
						onChange={formik.handleChange}
					/>
					{amenity.name}
				</label>
				<br />
			</div>
		)
	});

	// Return, etc.
};
```
The `formik.handleSubmit` prop should get added to the overall form's `onSubmit` attribute:

```javascript
// Search.js

// imports 

function Search() {
	// State-setting, useEffect fetching, Formik set-up, etc.
	// Checkboxes

	return (
		<div>
			<h1>Search</h1>
			<form onSubmit={formik.handleSubmit}>
				{checkboxes}
				<input type="submit" value="Search" />
			</form>
		</div>
	)
};
```
Now, let's hop back to our `useFormik` hook and take a closer look at `onSubmit`. Formik initially receives an array of strings from the form. We're going to want to take those individual strings and join them together into one string, which we can accomplish using the [`join()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join) method.

Here's what that updated code should look like now:

```javascript
// Search.js

// imports

function Search() {

	// State-setting, useEffect fetching

	const formik = useFormik({
		initialValues: {
			checked: []
		},
		onSubmit: (values) => {
			const value_array = values['checked'];
			console.log('Value Array:', value_array)
			const value_string = value_array.join();
			console.log('Value String:', value_string)
		}
	})

	// Checkboxes, return, etc.
	
};
```

And here's what output should look like when printed in the browser console:

![Screenshot of Search page with top three amenities checked, with the browser developer tools open at the side, showing a printout of the checkbox values in the console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3sn298d5l17zl9v15gtl.png)

Joining these ids together into one string will make communicating our request to the server much easier, as we'll discuss in the next part. 

Before I realized that I could pass this string of multiple ids as a parameter in my fetch request to my server, I tried setting up a series of fetch requests with each individual amenity ID to return arrays of matching park objects and then filtering through those arrays to find the intersecting records. I won't go into the nitty-gritty details here, but suffice it to say that, not getting the expected results 30 lines of code later, there is no straightforward way of accomplishing this kind of filtering with JavaScript. However, utilizing the powerful data manipulation capabilities of Python and SQL, this task is much simpler to accomplish on the backend!

## Backend

The backend of this project uses Python, Flask, and SQLAlchemy. *(Note: I won't get into the details of the initial configuration of those libraries but encourage you to check out the [Flask](https://flask.palletsprojects.com/en/2.2.x/) and [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/) docs for more background.)* 

### Create API Resource and Route

To build out the route that we'll use to return search results, we'll start by creating a new API resource, `'ParksByAmenityIds'`, with a `GET` method that takes a string of ids as a parameter (hey, that sounds like what we just made on the frontend!). Before building out the logic within that resource, let's immediately go ahead and add this resource and the corresponding endpoint below (it is very easy to forget otherwise!).

```python
# app.py

from flask import make_response
from flask_restful import Resource
from config import app, api
from models import Amenity

class ParksByAmenityIds(Resource):
	def get(self, id_string):
		pass

api.add_resource(ParksByAmenityIds, '/park_amenities/<string:id_string>')
```
With that out of the way, let's build out the underlying logic. 

### Build Out Query Logic

First, we'll split the `id_string` into an array of separate ids.
```python
id_array = id_string.split(',')
```
Next, we'll use a [list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp) to convert those id elements from strings into integers.
```python
int_ids = [int(id) for id in id_array]
```
Now, we can get a list of the parks that have any (but not all) of the specified amenities by querying the `ParkAmenity` join table. We can use the [`.in_` column operator](https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.ColumnOperators.in_) (which operates similarly to [Python's `in` keyword](https://www.w3schools.com/python/ref_keyword_in.asp)) to return all the parks with a matching amenity id. (For the example I used with amenity ids 1, 2, and 3, there were 276 items returned with duplicates.)

```python
all_matching_amenities = [amenity.to_dict() for amenity in ParkAmenity.query.filter(ParkAmenity.amenity_id.in_(int_ids)).all()]
```

Next, we'll use another list comprehension to create a new list of all the park IDs from the result of the `all_matching_amenities` query.
```python
all_park_ids = [element['park']['id'] for element in all_matching_amenities]
```

Using yet another list comprehension, let's return all of the ids which appear in the park ID list as many times as there are amenities we're searching for (in this case, we've got a list of three amenities). We're using the [`.count()` method](https://www.w3schools.com/python/ref_list_count.asp) as well as finding the length of our amenities list with [`len()`](https://www.w3schools.com/python/ref_func_len.asp) to make this comparison. The result will be a list of all the park ids connected to all the specified amenities.

```python
multiple_matches = [id for id in all_park_ids if all_park_ids.count(id) == len(int_ids)]
```

We can quickly remove all of the duplicates by turning that list into a set with [Python's `set()` constructor](https://www.w3schools.com/python/python_sets.asp).
```python
unique_matches = set(multiple_matches)
```

In this example, we've whittled down 276 items that match at least one criterion to a set of 18 items that match all three criteria — fantastic!

Using those unique matches, we can now query the `Parks` table (again using the `.in_` column operator) to return all the parks with an id appearing in our new set of unique matches. This list of park objects (with all their details, not just the ids) is what our server will send back to the frontend in response to our search request.

```python
parks = [park.to_dict() for park in Park.query.filter(Park.id.in_(unique_matches)).all()]
```

### Add Error Handling if No Matches Found
Before we tie this back together with our frontend, we should add some error handling. It will not be uncommon for users to select a combination of criteria that won't yield any matches. We'll want to let the frontend know if this is the case so that it can render an error message to the user accordingly. In this case, we'll add a clause that specifies that if the length of the list of parks returned is 0, the server should send back an error.

```python

if len(parks) == 0:
	response = make_response(
		{"error": "No matching parks."},
		404
		)
	return response

else:
	response = make_response(
		parks,
		200
		)
	return response
```

Here's what the entire resource looks like, all put together: 

```python
# app.py

# imports, additional resources/routes

class ParksByAmenityIds(Resource):
	def get(self, id_string):
		id_array = id_string.split(',')
		int_ids = [int(id) for id in id_array]

		all_matching_amenities = [amenity.to_dict() for amenity in ParkAmenity.query.filter(ParkAmenity.amenity_id.in_(int_ids)).all()]
		all_park_ids = [element['park']['id'] for element in all_matching_amenities]
		multiple_matches = [id for id in all_park_ids if all_park_ids.count(id) == len(int_ids)]
		unique_matches = set(multiple_matches)

		parks = [park.to_dict() for park in Park.query.filter(Park.id.in_(unique_matches)).all()]

		if len(parks) == 0:
			response = make_response(
				{"error": "No matching parks."},
				404
			)
			return response

		else:
			response = make_response(
				parks,
				200
			)
			return response

api.add_resource(ParksByAmenityIds, '/park_amenities/<string:id_string>')
```

## Tying the Frontend and Backend Together

Now that we've got this route built out, let's update the Formik `onSubmit` handler with that route to fetch the matching parks from the server. We'll add a clause for rendering an error on the search page if no matches are found. We'll set up `searchError` as a boolean state variable and have the error message render conditionally based on whether the error state is true.

```javascript
// Search.js

// imports

function Search() {

	const [amenities, setAmenities] = useState([])
    const [searchError, setSearchError] = useState(false)

	// useEffect fetching

	const formik = useFormik({
        initialValues: {
            checked: []
        },
        onSubmit: (values) => {
            const value_array = values['checked'];
            const value_string = value_array.join();
            fetch(`/park_amenities/${value_string}`)
                .then(response => {
                    if (response.ok) {
                        response.json()
                        .then(parkData => console.log(parkData))
                    } else {
                        response.json()
                        .then(error => setSearchError(true))
                    }
                })
        }
    })

	// Checkboxes

	return (
        <div>
            <h1>Search</h1>
            {searchError
                ? <p>No matches found.</p>
                : null
            }
            <form onSubmit={formik.handleSubmit}>
                {checkboxes}
                <input type="submit" value="Search" />
            </form>
        </div>
    )
};
```

To be able to render those matches in our 'Results' component, we'll need to establish `parks` as a state variable in the parent component (`App`) and pass those down to `Search` and `Results` as props.

```javascript
// App.js

// React Imports
import { React, useState } from 'react';

// Additional Imports

function App() {

	const [parks, setParks] = useState([])

	return (
	<Switch>
		<Route path='/search'>
			<Search setParks={setParks}/>
		</Route>
		<Route path='/results'>
			<Results parks={parks}/>
		</Route>
	</Switch>
	)
};

export default App;
```

Now, upon a successful search, we can use the `setParks` prop to save those matches in state. We'll also use the [`useHistory`](https://v5.reactrouter.com/web/api/Hooks/usehistory) hook from React Router to send us to the Results page upon a successful search using the `.push()` method.

```javascript
// Search.js

import { useHistory } from 'react-router-dom';
// Additional Imports

function Search({ setParks }) {
    // state-setting

	const history = useHistory()

    // useEffect fetching

    const formik = useFormik({
        initialValues: {
            checked: []
        },
        onSubmit: (values) => {
            const value_array = values['checked'];
            const value_string = value_array.join();
            fetch(`/park_amenities/${value_string}`)
                .then(response => {
                    if (response.ok) {
                        response.json()
                        .then(parkData => setParks(parkData))
                        .then(() => history.push('/results'))
                    } else {
                        response.json()
                        .then(error => setSearchError(true))
                    }
                })
        }
    })

	// checkboxes, return, etc.
};
```

Last, let's quickly get a basic `Results` component built out: 

```javascript
// Results.js

function Results({ parks }) {

    const li_parks = parks.map(park => {
        return <li key={park.id}>{park.name}</li>
    })
    return (
        <div>
            <h1>Results</h1>
            <ul>
                {li_parks}
            </ul>
        </div>
    )
};

export default Results;
```

And there we have it, a successful search — hopefully the first of many for our users!

![Screenshot of the results page, featuring a bulleted list of all 18 parks matching the search criteria](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fr2qkgc92r59gyc4udh.png)