Skip to content

Instantly share code, notes, and snippets.

@joerodrig
Last active March 16, 2016 03:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save joerodrig/957fbb79545ce216b421 to your computer and use it in GitHub Desktop.
Save joerodrig/957fbb79545ce216b421 to your computer and use it in GitHub Desktop.
React + Rails + Webpack and React-Refetch

Overview and Goals

React_On_Rails is an awesome library that integrates React + Rails + Webpack, with boilerplate to quickly get you up and running on a new project. React-Refetch is also another great library that makes reading and writing data to the server in React seamless. My goal here is to develop an architecture leveraging these two libraries that accomplishes three goals:

  1. Simple
  2. Easy to maintain
  3. Scalable

To accomplish this, our architecture will leverage React-Refetch and Rails routing to expose each component to the data needed on each given page, using our server data as the single source of truth, therefore minimizing or, idealy, completely removing our use of state to modify data on many pages. This reduces the fragile nature of offloading a batch of data to one large root component and dispersing it among nested sub-components. This also allows us to always have a 1-to-1 relationship between what the client sees and what exists on our server.

Note: This gist is still a work in progress and is based off of my current experience working with React_On_Rails and React-Refetch. Some parts may be incomplete or better solutions may exist. All feedback is appreciated :D.

Benefits

  • This approach allows us to organize our components in a clear, maintainable way by explicity declaring components and data for each component.
  • Components are as smart as they need to be. If you have multiple sub-pages, each sub-page only has access to the data that it needs via React-Refetch wrappers.

Downsides

  • For web-apps that will continue to grow in sub-pages and follow the file-per-component approach(followed by linters such as ESLint), the shallow folder architecture doesn't scale well.
  • Web-apps that attempt to offer optimistic updates will run into many limitations or have to go against the pattern followed by React-Refetch.

Outlining the Ideal Component Flow

  1. Navigating to a specific route will load an .erb template. This .erb template will have access to the 'page' props from the server as well as any other necessary info for that page.
# /config/routes.rb
resources :surveys, only: [:index, :create, :edit], controller: "surveys" do
  ...
end

# /controllers/surveys_controller.rb
def edit
  @props = { page: survey.survey_administration_page(params[:page]) }
end
  
# /models/survey.rb
# Allow for there to be multiple different sub-pages in the survey editor.
# The default page is the "editor" page
def survey_administration_page(current_page)
  current_page || "editor"
end
  1. The .erb template will load in a mirrored JSX interpretation of the erb template
    NOTE: In this case, instead of calling our SurveyPage component directly from our .erb template (which is OK), we add an extra component that mirrors the .erb template in our client view. There are a couple of benefits to this approach:
    a. It is much easier to keep track of which React components are related to a specific controller
    b. The require tree for components is more intuitive(see the code example in step 3)
# /views/surveys/edit.html.erb 
<%= react_component("SurveysEditPage", props: @props) %>
  1. The root React component passes these props down to a container which determines which sub-page to load. If the sub-page requires specific data from the server, a wrapper for the page will be called first with the associated fetch(es) and API calls. Otherwise, if the component is purely static data, we call the page directly.
// /client/.../views/surveys_edit_page.jsx
import SurveyPage from '../Pages/Survey/survey';

export default class SurveyEditPage extends React.Component {
  render() { return ( <SurveyPage {...this.props} /> ); }
}

// /client/.../Pages/Survey/Survey.jsx

// If we were to call these components from our /Views path, our relative path would be "../Pages/Surveys/Wrappers/survey_*"
// which feels less intuitive in my opinion(in reference to the note stated above).
import SurveySubmissionsWrapper from "./Wrappers/survey_submissions_wrapper";
import SurveyEditorWrapper from "./Wrappers/survey_editor_wrapper";

export default class Survey extends React.Component {
  // ...
  // Load in the proper page based off of the page props
  // If other important information is necessary for a page, pass that in as props as well.
  _page() {
    const { surveyId, page } = this.props;
    switch(page) {
      case "editor":
        return ( <SurveyEditorWrapper surveyId={surveyId} /> );
      case "submissions":
        return ( <SurveySubmissionsWrapper surveyId={surveyId} /> );
      default:
        return ( <SurveyEditorWrapper surveyId={surveyId} /> );
    }
  }

  render() {
    return (
      <div id="Survey">
        <SurveyNavbar />
        {this._page()}
      </div>
    );
  }
}

In the case above, both of our sub-pages being loaded require different data from the server. Rather than having our SurveyPage component load and pass that data directly into our sub-components, this is where we rely on React-Refetch to fetch that data for us via our Wrappers.

# /config/routes.rb
resources :surveys, only: [:index, :create, :edit], controller: "surveys" do
  # Survey editor
  get '/editor', to: 'surveys#editor_page'
  get '/submissions', to: 'surveys#submissions_page'
end
// /client/.../Pages/Survey/Survey.jsx
// ...
switch(page) {
  case "editor":
    return ( <SurveyEditorWrapper /> )
  case "submissions":
    return ( <SurveySubmissionsWrapper /> )
// ...

// /client/.../Pages/Survey/Wrappers/survey_editor_wrapper.jsx
export default connect(props => {
  const BASE_URL = "/surveys/" + props.surveyId;
  const PAGE_END_PATH = '/editor'
  return {
    surveyFetch: { url: BASE_URL + PAGE_END_PATH },
    // ...
  }(SurveyEditorPage)
# /controllers/surveys_controller.rb
def editor_page
  props = { survey: survey, questions: survey.sorted_survey_questions, ... }
  render :json =>  props
end

Again, the benefits to this approach result in no component being smarter than it needs to be, and we reduce the amount of components we need to track props through. Another major benefit is that adding new sub-pages is made extremely simple.

Folder Architecture

  1. Rails views should have a corresponding root component
  2. Wrappers should be nested in the pages they’re used in
/app
 /views  
   /surveys
     edit.erb
     show.erb
/client
  /…
    /Views
      survey_edit_page.jsx
      survey_show_page.jsx
    /Pages
      /Survey
        /Wrappers
      		...wrappers
      		survey_editor_wrapper.jsx
      		survey_submissions_wrapper.jsx
				Survey.jsx
				survey_editor_page.jsx
				survey_submissions_page.jsx
	    	...components
  /Utilities
    ...components
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment