Skip to content

An example of how to setup Phoenix and React to play nicely together

Notifications You must be signed in to change notification settings

urfolomeus/phoenix-react-example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phoenix React Example

This is an example of how to set up a Phoenix and React project. We'll start with a basic umbrella mix app and build from there. Technologies and the versions that this example uses are below.

Technologies

Languages

See the project's .tool-versions file.

Frameworks

  • Phoenix -> see the frontend app's mix.exs file.
  • React -> see the frontend app's package.json file.

Libraries

  • See each app's mix.exs file for Elixir libraries.
  • See the frontend app's package.json for JavaScript libraries.

Instructions

Please note that I've marked the commits with COMMIT n so that you can check the codebase if you need to.

Base umbrella app

  1. Navigate to the folder where you want to place the project and type:

    mix new --umbrella <project_name>
    cd <project_name>
  2. At this point you'll probably want to start a README for the project and initialize git. If you're using asdf to manage your versions, you can set this up now too.

    $EDITOR ./README.md
    git init
    asdf local elixir <version>
    asdf local erlang <version>
    asdf local nodejs <version>
  3. COMMIT 1

Adding Phoenix

  1. We'll now add a Phoenix app. Note that we're not using Ecto (we'll use a separate app to manage persistence) or Brunch (we'll be using Webpack instead).

    cd apps
    mix phoenix.new --no-ecto --no-brunch <frontend_app_name>
  2. COMMIT 2

Adding webpack

  1. Now we'll add Webpack, but before we can do that we need to initialize our application for npm.

    cd apps/<frontend_app_name>
    npm init # and follow the prompts
  2. We're going to use Webpack 2, so for the time being we'll need to specify the version. Check the Webpack GitHub repo for the latest stable version. We're going to use 2.2.0 here.

    npm i -D webpack@2.2.0
  3. If you do a git status now you'll see that we get a load of node_modules files. Let's set up the local .gitignore to ignore unwanted JavaScript files.

    # <project_name>/apps/<frontend_app_name>/.gitignore
    ...
    # Static artifacts
    /node_modules
    npm-debug.log*
    
    # Since we are building assets from web/static,
    # we ignore priv/static. You may want to comment
    # this depending on your deployment strategy.
    /priv/static/
    ...
  4. Now we need to setup webpack so that it will automatically build our assets when we make any changes to them. We'll start by adding a webpack.config.js file to the root of the frontend app. We're going to make the entry to our React app web/static/index.js even though Phoenix normall makes the root web/static/app.js. This is personal preference as we discovered that the app.js file ends up being the natural place to do all the app setup, whereas we just want index.js to be the leaping off point. We'll still call the build file in priv/static/js app.js because when concatenated that is our entire app. Feel free to change this though if you'd prefer.

    var path = require('path');
    
    module.exports = {
      entry: './web/static/js/index.js',
      output: {
        path: path.join(__dirname, 'priv', 'static', 'js'),
        filename: 'app.js'
      }
    };
  5. Then we can create a script to start our webpack watcher in package.json

    ...
    "scripts": {
      "start": "webpack --watch-stdin --progress --color"
    }
    ...
  6. And then call it from config/dev.exs in the watchers section, passing in the location of the root of the frontend app so that NPM knows where to find the package.json file.

    #...
      watchers: [  watchers: [npm: ["start", "--prefix=./apps/<app_name>"]]
    

] #... ```

  1. Finally we need to have a web/static/js/index.js file, because that's where we told webpack we'd have the entry point to our application.

    console.log('Webpack is working')
  2. If we start up our server (mix phoenix.server), visit http://localhost:4000 and then open the console we should see "Webpack is working".

  3. COMMIT 3

Adding Babel

  1. In order to use the built-in Phoenix JavaScript, and just to have a more pleasant coding experience, we'll use Babel to transpile ES6 JavaScript into ES5 JavaScript. We'll start by installing the required libraries from npm. Check that your still in the root of your frontend project app and then:

    npm i -D babel-core babel-loader babel-preset-env
  2. Now we need to wire up webpack to use Babel. In webpack.config.js

    var path = require('path');
    
    module.exports = {
      entry: './web/static/js/index.js',
      output: {
        path: path.join(__dirname, 'priv', 'static', 'js'),
        filename: 'app.js'
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            use: 'babel-loader',
            exclude: /node_modules/
          }
        ]
      },
      resolve: {
        modules: [ 'node_modules', __dirname + '/web/static/js' ]
      }
    };
  3. We can complete this step by adding a .babelrc file to the root of the frontend app with the following:

    {
      "presets": ["babel-preset-env"]
    }
  4. Check that everything has gone as planned by changed web/static/app.js to the following and then checking your browser console again.

    const test = () => console.log('Webpack is working with Babel')
    test()
  5. COMMIT 4

Adding CSS Processing

  1. Now let's setup webpack to post process our CSS for us. Typically in a React project you keep the CSS files alongside the code files, so we'll stick with this convention. However, rather than have the styles added to the HTML file in a style tag as is typically done in development React apps, we'll build to priv/static as that's what is normally done in a Phoenix app. Feel free to change this in you app if you prefer. Let's start by installing the necesarry libraries (note that we need to specify the version for extract-text-webpack-plugin).

    npm i -D css-loader style-loader extract-text-webpack-plugin@2.0.0-beta.5
  2. Now update the webpack.config.js file as follow:

    var ExtractTextPlugin = require('extract-text-webpack-plugin');
    var path = require('path');
    
    module.exports = {
      entry: './web/static/js/index.js',
      output: {
        path: path.join(__dirname, 'priv', 'static', 'js'),
        filename: 'app.js'
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            use: 'babel-loader',
            exclude: /node_modules/
          },
          {
            test: /\.css$/,
            loader: ExtractTextPlugin.extract({
              fallbackLoader: 'style-loader',
              loader: 'css-loader'
            })
          }
        ]
      },
      plugins: [
        new ExtractTextPlugin('../css/app.css')
      ],
      resolve: {
        modules: [ 'node_modules', __dirname + '/web/static/js' ]
      }
    };

    We add in a require for the ExtractTextPlugin and then use that in a new plugins section to generate a CSS file in priv/static/css/app.css. Then we add a new rule to the module section that matches on files ending .css anywhere in our web/static/js folder and runs them through first the css-loader and then the style-loader, finally handing off to the ExtractTextPlugin loader.

  3. Let's now actually output something on the screen so that we can tell if we have everything wired up correctly. We'll start by deleting what's currently being shown. Change web/templates/layouts/app.html.eex as follows:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="">
        <meta name="author" content="">
    
        <title>Hello Frontend!</title>
        <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
      </head>
    
      <body>
        <%= render @view_module, @view_template, assigns %>
        <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
      </body>
    </html>

    And then change web/templates/page/index.html.eex to the following:

    <div class="root">App goes here</div>
  4. We'll now create the skeleton for our React app. In web/static/js add a folder called app and then add the following files to it.

    app.css

    .root {
      color: red;
    }

    app.js

    import './app.css'
  5. And change web/static/js/index.js to the following:

    import './app/app'
  6. If you refresh your browser you should now see the text "App goes here" in red.

  7. We've not setup any special post processing rules here, but you might want to consider autoprefixing and possibly also CSS Modules.

  8. COMMIT 5

Adding image support

  1. React has a different way of handling images. They are typically imported in to the component file thaat needs them and then attached to the image. One way that is becoming popular is to use a library like image-webpack-loader to determine based on the size of a file whether to generate a url to an image or whether to turn it into a blob and attach that as data to the image tag. We'll set this up in webpack, but first we need the libraries.

    npm i -D image-webpack-loader url-loader
  2. Now open webpack.config.js and change it to the following:

    var ExtractTextPlugin = require('extract-text-webpack-plugin');
    var path = require('path');
    
    module.exports = {
      entry: './web/static/js/index.js',
      output: {
        path: path.join(__dirname, 'priv', 'static', 'js'),
        filename: 'app.js',
        publicPath: 'priv/static'
      },
      module: {
        rules: [
          {
            test: /\.jsx?$/,
            use: 'babel-loader',
            exclude: /node_modules/
          },
          {
            test: /\.css$/,
            loader: ExtractTextPlugin.extract({
              fallbackLoader: 'style-loader',
              loader: 'css-loader'
            })
          },
          {
            test: /\.(jpe?g|png|gif|svg)$/,
            use: [
              {
                loader: 'url-loader',
                options: { limit: 40000 }
              },
              'image-webpack-loader'
            ]
          }
        ]
      },
      plugins: [
        new ExtractTextPlugin('../css/app.css')
      ],
      resolve: {
        modules: [ 'node_modules', __dirname + '/web/static/js' ]
      }
    };

    We've added another rule, this time to match on image file extensions and use the url and image-webpack loaders to either attach the image as data if it's 40kb or under or as a url. We also had to a publicPath key to our output section so that webpack knows where to place any image file that we need.

  3. Let's test what we've built. Move the phoenix.png file from priv/static/images into web/static/assets/images (you may need to create the latter folder if you created your folder without Brunch).

  4. Now open web/static/js/app/app.js and change it to the following:

    import './app.css'
    
    import phoenix from '../../assets/images/phoenix.png'
    
    const image = document.createElement('img')
    image.src = phoenix
    document.body.appendChild(image)
  5. If you refresh your browser you should see that the Phoenix logo has been added to the page. If you look towards the bottom of the generated priv/static/js/app.js file you should see that the image was appended as data rather than as a URL because the file size is under 40kb.

  6. COMMIT 6

Adding React

  1. OK time to start building our React app. We'll start, as ever, by adding the required libraries.

    npm i --save react react-dom
    npm i -D babel-preset-react
  2. The second line above enables us to tell Babel that we're going to be using JSX so that it will transpile it to ES5 for us as well as the ES6 we'll be writing. Adjust your .babelrc accordingly.

    {
      "presets": ["babel-preset-env", "react"]
    }
  3. Now we can test that everything is wired up by building a simple React component. We're going to start using the directory structure that we've come to favour even though there's no need just now. Start as we mean to go on as it were. We'll start with a simple component. Inside web/static/js/app create a folder called components. Rename the web/static/js/app/app.css file to web/static/js/app/components/SayHello.css. Now rename web/static/js/app/app.js to web/static/js/app/components/SayHello.jsx and change it to the following:

    import React from 'react'
    
    import './SayHello.css'
    import phoenix from '../../../assets/images/phoenix.png'
    
    const SayHello = () =>
      <div>
        <h1>Hello, world!</h1>
        <img src={phoenix} />
      </div>
    
    export default SayHello
  4. You may have noticed that we're using the file extension .jsx on the component file. This is not necessary, you can use .js just fine. I personally find it helps me to know whether a file is basically a pure component (i.e. there's little to know additional JavaScript logic in there) or something more convoluted. If you do want to use .jsx file extensions then you'll need to let webpack know about it. In the resolve section of your webpack.config.js file add the following:

    extensions: ['.js', '.jsx']

  5. We now need a container to hook this up to. The reason for this will become apparent in the next section when we add Redux. Create a web/static/js/app/containers folder and add a file App.js to it. Add the following to it:

    import React from 'react'
    
    import SayHello from '../components/SayHello'
    
    const App =
      <SayHello />
    
    export default App
  6. In the root of the web/static/js/app folder create an index.js file with the content below:

    export { default as App } from './containers/App'

    By creating an index file for each folder we can access it's contents through a described API, thus shielding the rest of the application from any changes.

  7. Change web/static/js/index.js to the following:

    import ReactDOM from 'react-dom'
    
    import { App } from './app'
    
    ReactDOM.render(App, document.querySelector('.root'));
  8. That should now be everything wired up. Refreshing your browser you should see "Hello, world!" with the Phoenix logo underneath it.

  9. COMMIT 7

About

An example of how to setup Phoenix and React to play nicely together

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published