Use your React components into your elixir application, using webpack compilation, so :
- An isomorphic ready library (SEO/JS are now nice together), but with Elixir on the server side
- Just a Library, with a minimum constraint about your application organization and layout :
- use any javascript compiled language
- use any javascript routing logic or library
- you can use JS React rendered component only for parts of your webpage
- Nice fluent dev workflow, with :
- combined stacktrace : elixir | javascript
- hot loading on both server and browser
- NPM/Webpack as the only config for respectively dependencies/compilation
- A cool UI to have an overview of your compiled javascript application
- You do not have to think about the server side Javascript configuration, just write a webpack conf for the browser, and it is ready to use.
TODO List :
- Handle Source map in server side for combined stacktrace generation
- Add Source map in client side
- Currently the compiler compile in parallel server entry and client entries, but server side compilation does not handle cacheable and slow down very much the compilation
- handle css loader for
reaxt/style
loader, currently it is ignored in server side, which is a problem for URL remapping in CSS.
See https://github.com/awetzel/reaxt-example for a ready to use example application, but lets look into details and requirements.
In your mix.exs, add the dependency and the custom compiler for webpack:
- Add the
:reaxt
dependency to your project.deps and application.applications - Add
compilers: [:reaxt_webpack] ++ Mix.compilers
to your project, (:reaxtWebpack
for elixir < v1.0.3)
In your config/config.exs, link the reaxt application to the application containing the JS web app
config :reaxt,:otp_app,:yourapp
Create the good directory and file layout:
MIXROOT/web
MIXROOT/web/package.json
containing your app NPM dependenciesMIXROOT/web/webpack.config.js
containing only the client side logic, use "reaxt/style" instead of "style" loader to load your css. A typical output path is../priv/static
.MIXROOT/web/components
containing modules exporting React components
In your elixir code generating HTML :
- add
WebPack.header
in the<head>
- add a script with src
/your/public/path/<%= WebPack.file_of(:entry_name) %>
Then render your server side HTML :
# if web/components/thefile exports the react component
Reaxt.render!(:thefile,%{it: "is", the: "props"})
# if web/components/thefile exports an object containing a react component
# at the key "component_key"
Reaxt.render!({:thefile,:component_key},%{it: "is", the: "props"})
The function return a %{html: html,css: css,js_render: js_render}
, you have to add in the html :
- the css
<style><%= render.css %></style>
- the html in an identified block (
<div id="myblockid"><%= render.html %></div>
) - the client side rendering call with
<script><%= render.js_render %>("myblockid")</script>
For example, if you want a page entirely generated by the react
component exported at web/components/app.js
, then in your elixir web server, send :
EEx.eval_string("""
<html>
<head> <%= WebPack.header %>
<style><%= render.css %></style>
</head>
<body>
<div id="content"><%= render.html %></div>
<script src="/public/<%= WebPack.file_of(:main) %>"></script>
<script><%= render.js_render %>("content")</script>
</body>
</html>
""",render: Reaxt.render!(:app,%{my: "props"}))
Finally, you have to serve files generated by webpack :
plug Plug.Static, at: "/public", from: :yourapp
Then iex -S mix
and enjoy, but the best is to come.
When you serve files generated by webpack, use the plug
WebPack.Plug.Static
instead of Plug.Static
, it contains
an elixir implementation of
webpack-dev-server,
and a nice UI.
if Mix.env == :dev do
use Plug.Debugger
plug WebPack.Plug.Static, at: "/public", from: :myweb
else
plug Plug.Static, at: "/public", from: :myweb
end
Then go to http://yourhost/webpack to see a beautiful summary of your compiled js application.
Then configure in your application configuration :
config :reaxt,:hot,true
to enable that:- server and client side JS will be compiled each time you change files
- server side renderers will be restarted at each compilation
- client browser page will be reloaded, a desktop notification will be triggered
- The
/webpack
UI will be automatically reloaded if it is on your browser
config :reaxt,:hot,:client
to enable the same hot loading, but with webpack module hot loading on browser to avoid full page reload- use the webpack loader
react-hot-loader
to load your component to enable automatic browser hot reloading of your components - the
reaxt/style
loader for your css enable hot reloading of your css
- use the webpack loader
See a full example in reaxt-example
Reaxt provides facilities to easily customize the rendering process at the
server and the client side : this is done by attaching reaxt_server_render
and/or reaxt_client_render
to the module or object referenced by the first
argument of Reaxt.render!(
.
reaxt_server_render(arg,render)
will- take
arg
from the second argument ofReaxt.render
, - have to execute
render(component,param)
when the wanting handler and props are determined.param
is any stringifyable object passed to client rendering
- take
reaxt_client_render(props,render,param)
have to render the good selected component on the client side.props
is the initial props used in server rendering,render
is function you have to call to make the client react renderingparam
is the deserialized version of the third parameter of the callback inreaxt_server_render
To understand how they work, let's look at the default implementation of these functions (what happened when they are not implemented).
// default server rendering only take the exported module as the
// handler to render and the argument as the props
default_reaxt_server_render = function(arg,render){
render(<this {...arg}/>,null)
}
// default client rendering only take the exported module as the
// handler to render, the param is ignored
default_reaxt_client_render = function(props,render,param){
render(<this {...props}/>)
}
Now let's see an example usage of these functions : react-router
integration (Reaxt.render
second argument is the Path):
See a full example in reaxt-example
Reaxt.render!(:router_handler,full_path(conn))
var App = require("./app")
var Router = require("react-router")
var Routes = require("./routes")
module.exports = {
reaxt_server_render: function(path,render){
Router.run(Routes, path,function (Handler, state) {
render(<Handler/>)
})
},
reaxt_client_render: function(props,render){
Router.run(Routes,Router.HistoryLocation,function(Handler,state){
render(<Handler {...props}/>)
})
}
}
JS exceptions and stacktraces during rendering are converted into Elixir one with a fake stacktrace pointing to the generated javascript file.
This is really nice because you can see javascript stacktrace in the Plug.Debugger
UI on exception.
You can define a term in Elixir using Application env
global_config
which will be available globally in the server and
the client side with require('reaxt/config')
.
config :reaxt,:global_config, %{
some_config1: "value1",
some_config2: "value2"
}
This configuration is static once the :reaxt
application has started. So if
you want to change this configuration at runtime, you need to reload all
:reaxt
renderer with Reaxt.reload
. Remember : this is a costly reload, do
not use it to maintain a state at real time but only for configuration purpose.
Application.put_env :reaxt, :global_config, %{
some_config1: "value3",
some_config2: "value4"
}
Reaxt.reload
Then in your javascript component, you can use this config using :
require('reaxt/config').some_config1
The NodeJS renderers are managed in a pool (to obtain "multicore" JS rendering), so :
config :reaxt,:pool_size
configure the number of worker running permanentlyconfig :reaxt,:pool_max_overflow
configure the maximum extension of the pool when query happens an it is full
A clever configuration could be :
config :reaxt,:pool_size, if(Mix.env == :dev, do: 1, else: 10)
For minification, remember that webpack compilation is launched by Mix, so you
can use process.env.MIX_ENV
in your webpack config.
{
externals: { react: "React" },
plugins: (function(){
if(process.env.MIX_ENV == "prod")
return [new webpack.optimize.UglifyJsPlugin({minimize: true})]
else
return []
})()
}