Change from default (vuejs) to react with the following commands in your terminal
php artisan preset react
npm install && npm run dev
First, rename the resources/js
to resources/react-app
as react may contain non js scripts as well. You may delete the files resources/react-app/components/Example.js
and resources/react-app/app.js
if it exists.
Clean up Laravel generated assets as well by deleting the following:
public/css/*
public/js/app.js
We will follow a convention which will require us to rename bootstrap.js
to bootstrap-spa.js
so rename the file now.
Create a new file resources/react-app/components/RootComponent.js
with the following contents:
File: resources/react-app/components/RootComponent.js
import React, { Component } from 'react';
class RootComponent extends Component {
render() {
return (
<div>
I'm the root component! Have the Front-end team replace me with their own.
</div>
);
}
}
export default RootComponent
Create a new file resources/react-app/index-spa.js
with the following contents:
File: resources/react-app/index-spa.js
require('./bootstrap-spa');
import React from 'react';
import ReactDOM from 'react-dom';
import RootComponent from './components/RootComponent';
if (document.getElementById('root')) {
ReactDOM.render(<RootComponent />, document.getElementById('root'));
}
Update webpack.mix.js
with the following:
Note: the following script might be overwhelming. Just keep in mind that most of the added scripts are for us to work around some of Laravel's still existing issues when enabling hot reloading which we will test later on.
File webpack.mix.js
const mix = require("laravel-mix");
/*
|--------------------------------------------------------------------------
| Env Management
|--------------------------------------------------------------------------
|
| Configurations taken from Laravel's .env or the server's environment
| variables.
|
*/
const WEBPACK_DEV_SERVER_PORT = parseInt(process.env.WEBPACK_DEV_SERVER_PORT || 8080);
const env = {
publicPath: `${process.env.APP_URL}:${WEBPACK_DEV_SERVER_PORT}/`,
isHttps: (process.env.WEBPACK_IS_HTTPS == true),
webpackDevServerPort: WEBPACK_DEV_SERVER_PORT,
webpackDevServerHost: process.env.WEBPACK_DEV_SERVER_HOST
};
Config.hmrOptions.port = env.webpackDevServerPort;
/*
|--------------------------------------------------------------------------
| Webpack Config for Hot Reloading
|--------------------------------------------------------------------------
|
| We'll enable a webpack-dev-server that will serve hot loaded assets
| to our laravel application. Note that laravel generates a "hot"
| file inside the /public/ folder.
|
| IMPORTANT!!!
| Currently, Laravel hardcodes the value of the "hot" file so you will
| have to replace its contents to match our configuration if it is
| not the same as defaults. If you don't mix() or ssr() helpers
| wont properly locate the compiled assets from webpack.
|
*/
mix.webpackConfig({
output: {
publicPath: env.publicPath
},
devServer: {
hot: true,
inline: true, // use inline method for hmr
disableHostCheck: true,
contentBase: path.join(__dirname, "public"),
https: env.isHttps,
port: env.webpackDevServerPort,
host: env.webpackDevServerHost,
headers: { "Access-Control-Allow-Origin": "*" },
watchOptions: {
exclude: [/bower_components/, /node_modules/]
}
},
node: {
fs: "empty",
module: "empty"
}
});
// Per this issue: https://github.com/JeffreyWay/laravel-mix/issues/1483
Mix.listen("configReady", webpackConfig => {
if (Mix.isUsing("hmr")) {
// Remove leading '/' from entry keys
webpackConfig.entry = Object.keys(webpackConfig.entry).reduce(
(entries, entry) => {
entries[entry.replace(/^\//, "")] = webpackConfig.entry[entry];
// }
console.log(entries);
return entries;
},
{}
);
// Remove leading '/' from ExtractTextPlugin instances
webpackConfig.plugins.forEach(plugin => {
if (plugin.constructor.name === "ExtractTextPlugin") {
console.log(plugin.filename);
plugin.filename = plugin.filename.replace(/^\//, "");
console.log(plugin.filename);
}
});
}
});
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.react("resources/react-app/index-spa.js", "public/js");
If you're running the application locally and don't have https yet, add the following configuration in your .env file:
File .env
WEBPACK_IS_HTTPS=false
Webpack dev server's default port is 8080 (more on this later), if you want to change this, add the following in your .env file:
File .env
WEBPACK_DEV_SERVER_PORT=<your port here>
Add the webpack dev server's host (defaults to localhost). This is important to be set if you're running this inside a docker container.
File .env
WEBPACK_DEV_SERVER_HOST=<ip>
Make sure this port is exposed if you're running this in a docker container.
Run the command:
npm run dev
This will transpile a production version of our scripts and put the files to public/js
as we configured at the end of webpack.mix.js
file.
Webpack generates our scripts for us, add them to the .gitignore so that the are not included to our source control.
File: .gitignore
... Laravel's default ignored files, dont remove them, append only what's below:
/public/js/index-spa.js
/public/js/index-ssr.js
/public/js/hydrator.js
Note: we deliberately did not create a folder react or a folder where we can just ignore the contents. This is so that the front-end being made from React is not so obvious.
Replace the contents of the welcome page resources/views/welcome.blade.php
with the following:
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Application</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<div id="root"></div>
<script src="{{ mix('js/index-spa.js') }}"></script>
</body>
</html>
Laravel has several open issues about hot reloading with react but we've pretty much resolved them already.
To "watch" updates to our files, just run:
npm run watch
This will have the terminal "watch" for changes in our files and retranspile our scripts automatically and more efficiently (you'll notice that it's faster than running npm run dev
or npm run prod
). Refresh the browser and the changes should reflect.
We also have the option to have the view automatically reflect changes without us refreshing the browser. This is called "hot reloading".
To enable, first install webpack-dev-server
with the command:
npm install -D webpack-dev-server
Then run hot reloading using the command:
npm run hot
This will take several seconds slower than npm run watch
. Wait for it to say "DONE Compiled successfully in ???ms??:??:??"
The hot reloading command will cause webpack to spin up a dev server running on a port we specify in our HOT_PORT
env variable. This server will host our assets for us instead of replacing the scripts in our resources/js
folder.
Refresh your browser one more time so that Laravel will take the assets from the webpack dev server instead of its own server. You may open the console and also notice logs like: [HMR] Waiting for update signal from WDS...
. This is your sign that hot reloading is properly working and is now listening to events from webpack dev server that triggers whenever we update something.
At this point, try updating anything in the RootComponent
and changes should reflect without us refreshing the server.
You may notice that instead of just replacing the content of the DOM, hot reloading will cause the browser to do a full reload instead. This is usually not desirable. A good sign of this is the log: [HMR] Cannot apply update. Need to do a full reload!
before refreshing.
To resolve this, add module.hot.accept()
if the application is hot reloading in the resources/react-app/index-spa.js
. See full update below:
File resources/react-app/index-spa.js
require('./bootstrap-spa');
import React from 'react';
import ReactDOM from 'react-dom';
import RootComponent from './components/RootComponent';
if (module.hot) {
module.hot.accept();
}
if (document.getElementById('root')) {
ReactDOM.render(<RootComponent />, document.getElementById('root'));
}
After updating, run npm run hot
again. Refresh your browser and your next updates should now happen without react manually refreshing the browser and instead just refresh the components that did change.
First of, we'll need spatie's ssr package to enable running node applications on the background from php for us.
Install the dependency using:
composer require spatie/laravel-server-side-rendering
php artisan vendor:publish --provider="Spatie\Ssr\SsrServiceProvider" --tag="config"
First you'll need to pick an engine to execute your scripts. The server-side-rendering library ships with V8 and Node engines. By default, the package is configured to use node, since you probably already have that installed on your system.
Set up the NODE_PATH environment variable in your .env file to get started:
Inside your .env:
NODE_PATH=/path/to/my/node
You'll also need to ensure that a storage/app/ssr folder exists, or change the ssr.node.temp_path config value to something else.
Create a new file called index-ssr.js
. As the name implies, this is the ssr version of our index-spa.js
, hence, why we had this naming convention earlier.
File: index-ssr.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import RootComponent from './components/RootComponent';
// We're telling spatie's ssr to take whatever markup we pass in here
// will be served when we call the ssr(/*...*/)->render() in PHP
dispatch(ReactDOMServer.renderToString(<RootComponent />));
IMPORTANT! Notice that we don't load the bootstrap-spa.js
file, this is because window
does not exist in node. This will also mean that whatever we attach to axios in the bootstrap will not take effect in SSR. If you need to do some tasks similar to what bootstrap-spa.js
do in SSR, create a separate file for it called bootstrap-ssr.js
to follow our convention.
In our case, we deliberately did not include axios for dynamic loading of contents from Laravel later. By default, SSR will execute all commands (INCLUDING AJAX calls) in the server which we find very resource exhaustive when we can just make use of spatie
's context
which we will demonstrate later.
And update our view to render the markup from the server with:
{!! ssr('js/index-ssr.js')->render() !!}
File: resources/views/welcome.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Application</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<div id="root">
{!! ssr('js/index-ssr.js')->render() !!}
</div>
<!-- <script src="{{ mix('js/index-spa.js') }}"></script> -->
</body>
</html>
Add this file to Laravel's mix (webpack.mix.js
):
mix
.react("resources/react-app/index-spa.js", "public/js")
.react("resources/react-app/index-ssr.js", "public/js") // added
Note: we commented out the spa for us to test that react really renders
Transpile the files for SSR:
npm run dev
Make sure that SSR is enabled (it's enabled by default only in production).
File: config/ssr.php
/*
* Enable or disable the server renderer. Enabled in production by default.
*/
// 'enabled' => env('APP_ENV') === 'production',
'enabled' => true,
This will result in the markup being rendered, but we have a new problem. React's events does not work anymore, for this, we'll need to hydrate
React.
To simulate events inside React, let's add a new component that makes use of React events and state management.
New FIle: resources/components/ClickDemo.js
import React, { Component } from 'react';
class ClickDemo extends Component {
constructor(props) {
super(props);
this.state = { clicks: 0};
}
onClick() {
let clicks = this.state.clicks + 1;
this.setState({ clicks });
}
render() {
return (
<div>
<button onClick={this.onClick.bind(this)}>Click to Increment Count</button>
Number of clicks so far: {this.state.clicks}
</div>
);
}
}
export default ClickDemo
Then include the ClickDemo
component in our RootComponent
like so:
File: resources/components/RootComponent.js
import React, { Component } from 'react';
import ClickDemo from './ClickDemo';
class RootComponent extends Component {
render() {
return (
<div>
I'm the root component! Have the Front-end team replace me with their own.
<ClickDemo />
</div>
);
}
}
export default RootComponent
Rebuild the app by npm run watch
, npm run dev
or npm run prod
and try it out on the browser, you should notice that clicking the button won't have any effect.
To "re attach" events to React components that are already rendered, we hydrate
it.
Create a new file resources/react-app/hydrate.js
.
File resources/react-app/hydrate.js
:
require('./bootstrap-spa');
import React from 'react';
import ReactDOM from 'react-dom';
import RootComponent from './components/RootComponent';
if (document.getElementById('root')) {
ReactDOM.hydrate(<RootComponent />, document.getElementById('root'));
}
Then add this to our webpack.mix.js
config:
File /webpack.mix.js
// ...
mix
.react("resources/react-app/index-spa.js", "public/js")
.react("resources/react-app/index-ssr.js", "public/js")
.react("resources/react-app/hydrate.js", "public/js") // added
Finally, import the hydrate script in place of index-ssr.js
.
IMPORTANT! Also remove whitespace inside the root element, React has a bug in dev mode that recognizes this whitespace as an error, see below:
File: resources/views/welcome.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Application</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body>
<!-- Notice removed whitespaces/new lines inside root -->
<div id="root">{!! ssr('js/index-ssr.js')->render() !!}</div>
<script src="{{ mix('js/hydrate.js') }}"></script>
</body>
</html>
Now try testing your application and increment should now work.
This portion will have too many code to put to a documentation so we'll be referencing the source code directly. Make sure you're either in the master
branch or in the feature-ssr-spa-hybrid
branch.