At the time of writing, I'm currently a college student with relatively little experience. The learning goal of this project is to prepare myself for my upcoming internship by using different frameworks and languages (React, Ruby, Rails, and GraphQL) as well as practicing my written communication. Due to the exploratory approach and my lack of experience, beware that my code may not make the best use of the frameworks or maximize efficiency.
This write-up is most similar to a journal entry rather than a tutorial or a textbook.
- ECMAScript: the scripting language that forms the basis of JavaScript.
- JavaScript ES6: the version of Javascript that is based on the 2015 version of ECMAScript.
New versions of JavaScipt offer new syntax and features. This can lead to some compatibility issues when different browsers do not yet support a version of JavaScript. Transpilers solve this issue by translating incompatible Javascript code into a version the browser supports.
Prior versions of Rails used Webpacker and Babel to manage and take care of transpiling and bundling JavaScript code (which added a lot of complexity). JavaScript ES6 and HTTP2 are now supported by most major browsers. The advantage of HTTP2 is that "you no longer pay a large penalty for sending many small files instead of one big file". This means that we no longer need a transpiling or bundling step (off you go Webpacker and Babel)!
Rails 7 now uses import maps to import JavaScript modules directly from the browser.
Unfortunately, React often uses JSX which requires a transpiling step. Thankfully, we can use htm (Hyperscript Tagged Markup) to use JSX-like syntax without any transpiling.
First, let's set up a components
folder to work inside our Rails 7 app.
rails generate controller components index
We can also organize the components
path inside our import map.
# config/importmap.rb
pin_all_from "app/javascript/components", under: "components"
Let's import the JavaScript modules we will need.
./bin/importmap pin react react-dom htm
Then, we create a function by binding htm to createElement
. Recall that each JSX element is just syntactic sugar for calling React.createElement(). We now have a function that can be used to produce objects in a JSX format.
// app/javascript/components/htm_create_element.js
import { createElement } from "react"
import htm from "htm"
export default htm.bind(createElement)
Here's a traditional React example:
// app/javascript/components/example.js
import { useState } from 'react';
export default function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Here's the modified version using htm.
// app/javascript/components/example.js
import { useState } from 'react';
export default function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return h`
<div>
<p>You clicked ${count} times</p>
<button onClick=${() => setCount(count + 1)}>
Click me
</button>
</div>
`;
}
Let's now use Example
at the root of our app. First, we define the root path.
# config/routes.rb
root "components#index"
We create a root element for our components
view.
# app/views/components/index.html.erb
<div id="root"></div>
And finally, add Example
to our components
index.
// app/javascript/components/index.js
import { render } from "react-dom"
import h from "components/htm_create_element"
import Example from "components/example"
render(
h`
<${Example}/>
`,
document.getElementById("root")
)
Run the server using the command below and check out http://localhost:3000/ to see Example
.
rails s
We've successfully used React and Rails 7 without transpiling!
GraphQL is a query language for your API, and a server-side runtime for executing queries. Its schema is in the form of a directed graph.
A traditional REST API can bring two issues:
Overfetching: the client fetches more information than they need.
"Imagine for example a screen that needs to display a list of users only with their names. In a REST API, this app would usually hit the
/users
endpoint and receive a JSON array with user data. This response however might contain more info about the users that are returned, e.g. their birthdays or addresses - information that is useless for the client because it only needs to display the users’ names."Underfetching: the client doesn't get enough information from a specific endpoint and needs to make additional requests to fetch the information they need.
"Consider the same app which needs to display the last three followers per user. The API provides the additional endpoint
/users/<user-id>/followers
. In order to be able to display the required information, the app will have to make one request to the/users
endpoint and then hit the/users/<user-id>/followers
endpoint for each user."
In GraphQL, a query simply represents a sub-graph of the schema so you fetch only the data you need without any additional query which effectively avoids overfetching/underfetching.
"Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI." The main advantage is the ability to store GraphQL queries into an in-memory cache which avoids sending additional network requests.
Let's assume we already have a Rails 7 app with models generated and database initialized.
First, we add the graphql-ruby gem.
bundle add graphql
rails generate graphql:install
bundle install
Then, create a GraphQL Type
for each model in your app.
rails generate graphql:object <model_name>
Lastly, add the following to allow access to the API from outside the domain while preventing Cross-Site Request Forgery (CSRF).
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
protect_from_forgery with: :null_session
Inside app/graphql/types/
, you can now modify each Type
for each model in your app to your liking. There, you can also add functions you want to use to fetch your data.
You can also specify at which endpoint the GraphQL server will operate from by modifying the line below (here it's /graphql
).
Rails.application.routes.draw do
...
post "/graphql", to: "graphql#execute"
...
end
You've now successfully set up a GraphQL server!
Let's set up Apollo Client. First let's import the required package.
./bin/importmap pin @apollo/client
Then, we create a client.
// app/javascript/components/apollo_client.js
import {
ApolloClient,
InMemoryCache,
} from "@apollo/client";
const client = new ApolloClient({
uri: '<your_app_uri>/graphql',
cache: new InMemoryCache()
});
export default client
Here is how you could use Apollo Client to query from the GraphQL server inside a React component.
// app/javascript/components/example.js
import {
useQuery,
gql
} from "@apollo/client";
import client from "components/apollo_client"
const QUERY = gql`
users {
id
followers {
id
}
}
`
export default Example() {
const { loading, error, data } = useQuery(QUERY, { client: client })
return ...
}
Tada, you've successfully set up Rails 7 with GraphQL, Apollo Client, and React!
I used react-pageflip to animate the recipe book. In order to so, simply add the following.
# config/importmap.rb
...
pin "react-pageflip", to: "https://ga.jspm.io/npm:react-pageflip@2.0.3/build/index.js"
pin "page-flip", to: "https://ga.jspm.io/npm:page-flip@2.0.7/dist/js/page-flip.browser.js"
...
- Refactor code
- Add helpful comments
- Style components (look into Tailwind CSS?)
- Modify ingredients schema (the measurements allowed are only mg right now)
- Improve animations (turning pages should hide the pages below)
- Add a way to add recipes (GraphQL Mutations)