DEV Community

Cover image for Sending data from React to Rails and vice versa.
Kim Nguyen
Kim Nguyen

Posted on • Updated on

Sending data from React to Rails and vice versa.

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:

data flow

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
Enter fullscreen mode Exit fullscreen mode

Generate our app:

create-react-app hipstew_frontend
Enter fullscreen mode Exit fullscreen mode

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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And add a custom route in our config/routes.rb:

get "/getrecipe", to: 'spoonacular_api#get_recipe'
Enter fullscreen mode Exit fullscreen mode

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:

hipstew-demo

Thank you for reading, feel free to comment below for further discussion. Stay tune for more :)!!

Top comments (2)

Collapse
 
sinrock profile image
Michael R.

Excellent information, thank you so much for taking the time to share this!!

Collapse
 
kimmese profile image
Kim Nguyen

Thank you so much 🙂!