During my final project at Flatiron School, I decided to build a foody app called Hipstew, which allows user to search for recipes based on given ingredients and they can create a list of their favorite recipes as well! And I've been wanting to work with a larger database for a while because I feel like I did not have a chance to dive deep into handling data in general. That is why, in my final project, I decided to cooperate Spoonaclar API into my app. Honestly, I was freaking out at the beginning of building this app: I had no idea how to send user's input from React to Rails nor how do I use that input to perform a GET request to my chosen API for data and send it back to React. But after some research and lectures, I finally figured it out and today, I made this tutorial to walk you through it step by step. A general flow of how React communicates with Rails:
Okay, let's dive into it:
Create React app:
There are several ways to generate a React app, there is no correct way to do it, but I usually use a project generator tool called create-react-app, developed by Facebook. To install:
npm install -g create-react-app
Generate our app:
create-react-app hipstew_frontend
In my frontend, I created a components folder to store all my future components. For this example, I created a SearchBar.js class component which has a controlled form to keep track of user's input and a submit function inherited from App.js:
import React from 'react'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import InputGroup from 'react-bootstrap/InputGroup'
export default class SearchBar extends React.Component {
state={
ingredient: ''
}
handleChange = (e) => this.setState({ ingredient: e.target.value})
render(){
return(
<div>
<InputGroup className="mb-3 search-bar">
<Form.Control
className="mb-2"
id="inlineFormInputName2"
placeholder="Ingredient name (beef, tomato, etc)"
value={this.state.ingredient}
onChange={this.handleChange}
/>
</InputGroup>
<InputGroup.Append>
<Button
variant='primary'
type="submit"
className="mb-2"
onClick={(e) => {
this.props.handleSubmit(e, this.state.ingredient)
this.setState({ ingredient: '' })
}}>
Submit
</Button>
</InputGroup.Append>
</div>
)
}
}
Note: I used some React Bootstrap here but it's optional! You can always user <form>
and <button>
instead of <InputGroup>
and <Button>
App.js component:
import React from 'react'
import SearchBar from './SearchBar'
import RecipeList from './RecipeList'
export default class App extends React.Component {
state={
ingredients: '',
recipe: ''
}
handleSubmit = (e, ingredients) => {
e.preventDefault()
this.setState({ ingredients, recipe: 'result' })
}
render(){
return(
<div>
<SearchBar handleSubmit={this.handleSubmit} />
{ this.state.recipe === 'result' ? <RecipeList ingredients={this.state.ingredients} /> : null }
</div>
)
}
}
In my App component, I used a recipe state to conditionally render RecipeList component. This component will only be rendered if a user submitted information in search bar.
RecipeList component:
import React from 'react'
export default class RecipeList extends React.Component {
state={
recipes: [],
error: null
}
componentDidMount(){
fetch(`http://localhost:3000/getrecipe?ingredients=${this.props.ingredients}`)
.then(resp => resp.json())
.then(data => {
// handling errors if any.
if (data.error){
this.setState({ error: data.error })
} else {
this.setState({ recipes: data })
}
})
}
render(){
return(
// render recipe based on data got back from rails.
)
}
}
This is where we actually send user's input to our Rails backend! I did a fetch request to a custom endpoint: '/getrecipe', but how do we send out user's input as params? Pretty similar to API endpoints, we can add a '?' + params name=data to send data to backend. For this case: /getrecipe?ingredients=${this.props.ingredients}
. I also use componentDidMount
lifecycle component to make sure RecipeList receives search's result before it's rendered (read more about lifecycle here). And that's the basic set up for our frontend. Let's also prepare our Rails app!
Create Rails app:
rails new hipstew_backend --api -T --database=postgresql
In this example, I use Postgres instead of SQLite, but this part is optional. If you do want to use Postgres, make sure you downloaded it here and have it running during this progress.
In our backend set up, beside my other models' controllers, I generated an extra controller dedicated to making requests to Spoonacular API, I named it spoonacular_api_controller but you can call it anything you want, make sure to use snake case for it :
rails g controller spoonacular_api_controller --no-test-framework
This would give us a barebone controller, nothing special yet. Let's add a function in our controller that performs a GET request to our API:
require './lib/key.rb'
class SpoonacularApiController < ApplicationController
BASE_URL='https://api.spoonacular.com'
def get_recipe
ingredientString = params["ingredients"].split(', ').map do |ing|
if ing.include?(' ')
ing[' '] = '-'
end
ing + '%2C'
end.join()
url = "#{BASE_URL}/recipes/findByIngredients?apiKey=#{API_KEY}&ingredients=#{ingredientString}&number=54"
response = HTTP.get(url)
data = response.parse
if data.length === 0
render json: {error: "There is no recipe for #{params["ingredients"]}. Please try another search term."}
else
render json: data
end
end
end
And add a custom route in our config/routes.rb:
get "/getrecipe", to: 'spoonacular_api#get_recipe'
This indicates whenever we fetch to '/getrecipe' endpoint, 'get_recipe' will be invoked!
At this point, if we put a byebug
inside get_recipe
and type params["ingredients"]
, we should get back our user's input from React app! I added ingredientString to make sure all ingredients are in camel-case.
Additional Note: make sure you store your API key in a separate file and include that file in .gitignore to keep your API key a secret. I stored mine in lib/key.rb!
Here is a my app in action using the example above:
Thank you for reading, feel free to comment below for further discussion. Stay tune for more :)!!
Top comments (2)
Excellent information, thank you so much for taking the time to share this!!
Thank you so much 🙂!