├── postcss.config.js ├── src ├── style.scss ├── index.js ├── App.js └── components │ └── Posts.js ├── .babelrc ├── .gitignore ├── package.json ├── README.md ├── plugin.php └── webpack.config.js /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | $primary-color: white; 2 | $bg: black; 3 | 4 | h1 { 5 | color: $primary-color; 6 | background-color: $bg; 7 | padding: 15px; 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react" 10 | ], 11 | "plugins": ["react-hot-loader/babel"] 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | coverage/ 3 | dist/* 4 | !dist/index.html 5 | node_modules/ 6 | *.log 7 | 8 | # OS generated files 9 | .DS_Store 10 | .DS_Store? 11 | ._* 12 | .Spotlight-V100 13 | .Trashes 14 | ehthumbs.db 15 | Thumbs.db 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "react-hot-loader/patch"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import App from "./App"; 5 | import "./style.scss"; 6 | 7 | var mountNode = document.getElementById("hmr-app"); 8 | ReactDOM.render(, mountNode); 9 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { hot } from "react-hot-loader/root"; 3 | import { Button } from "@wordpress/components"; 4 | import Posts from "./components/Posts"; 5 | 6 | function App({ name }) { 7 | return ( 8 | <> 9 |

Hello {name}!

10 | 11 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default hot(App); 21 | -------------------------------------------------------------------------------- /src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { __ } from "@wordpress/i18n"; 3 | import apiFetch from "@wordpress/api-fetch"; 4 | import { Spinner } from "@wordpress/components"; 5 | 6 | function Posts() { 7 | const [posts, setPosts] = useState([]); 8 | const [isFetching, setIsFetching] = useState(true); 9 | 10 | useEffect(() => { 11 | setIsFetching(true); 12 | 13 | apiFetch({ 14 | path: "/wp/v2/posts", 15 | }) 16 | .then((resp) => { 17 | setIsFetching(false); 18 | setPosts(resp); 19 | }) 20 | .catch((err) => { 21 | setIsFetching(false); 22 | console.log(err); 23 | }); 24 | }, []); 25 | 26 | if (isFetching) { 27 | return ( 28 |
29 | {__("Loading posts...")} 30 |
31 | ); 32 | } 33 | 34 | return ( 35 |
36 |

{__("Blog Posts")}

37 | 38 | 48 |
49 | ); 50 | } 51 | 52 | export default Posts; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-react-hmr-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "keywords": [], 7 | "author": "", 8 | "license": "ISC", 9 | "scripts": { 10 | "clean": "rm -rf ./dist", 11 | "build:dev": "webpack --mode development", 12 | "build": "webpack --mode production", 13 | "start": "webpack serve --hot --mode development --disable-host-check" 14 | }, 15 | "dependencies": { 16 | "@wordpress/api-fetch": "^3.21.1", 17 | "@wordpress/components": "^12.0.1", 18 | "@wordpress/i18n": "^3.17.0", 19 | "react": "^17.0.1", 20 | "react-dom": "^17.0.1", 21 | "react-hot-loader": "^4.13.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.12.10", 25 | "@babel/preset-env": "^7.12.11", 26 | "@babel/preset-react": "^7.12.10", 27 | "@hot-loader/react-dom": "^17.0.1+4.13.0", 28 | "autoprefixer": "^10.2.1", 29 | "babel-loader": "^8.2.2", 30 | "clean-webpack-plugin": "^3.0.0", 31 | "copy-webpack-plugin": "^7.0.0", 32 | "css-loader": "^5.0.1", 33 | "file-loader": "^6.2.0", 34 | "mini-css-extract-plugin": "^1.3.3", 35 | "node-sass": "^5.0.0", 36 | "postcss-loader": "^4.1.0", 37 | "prettier": "^2.2.1", 38 | "sass-loader": "^10.1.0", 39 | "style-loader": "^2.0.0", 40 | "url-loader": "^4.1.1", 41 | "webpack": "^5.12.2", 42 | "webpack-cli": "^4.3.1", 43 | "webpack-dev-server": "^3.11.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React HMR WordPress Plugin 2 | 3 | A WordPress plugin built with React with [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement/) support. 4 | 5 | ### Instructions 6 | 7 | 1. Clone the repository in your `/wp-content/plugins` folder. 8 | 1. cd into the repo in your terminal `cd wp-react-hmr-demo` 9 | 1. Install dependencies by running `yarn install` 10 | 1. Build the plugin assets: `yarn build` 11 | 1. Install the plugin from `/wp-admin/plugins/` page 12 | 13 | To start developing with [HMR](https://webpack.js.org/concepts/hot-module-replacement/) support, open your `wp-config.php` and add this constant: 14 | 15 | ```php 16 | define( 'WP_LOCAL_DEV', true ); 17 | ``` 18 | 19 | Now run: `yarn start` 20 | 21 | If you navigate to the plugin's menu and start changing your React code, you should see the code updates in realtime. Enjoy! 22 | 23 | ##### What is this `WP_LOCAL_DEV` constant? 24 | 25 | When you start the webpack-dev-server with `yarn start` command, it opens a server in `http://localhost:8080` and listens to the changes. The `WP_LOCAL_DEV` constant helps to detect if we are in the development environment. If configured, the plugin CSS and JS files are served from `http://localhost:8080/` instead of your WordPress installation. 26 | 27 | When not running `yarn start`, you should turn the constant to `false`. 28 | 29 | ### Commands 30 | 31 | `yarn start`: Starts in development mode with HMR support 32 | 33 | `yarn build`: Runs the build in production mode 34 | 35 | `yarn build:dev`: Runs the build in development mode 36 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | Hello World'; 29 | } 30 | 31 | function react_hmr_scripts() 32 | { 33 | $version = date('y-m-d'); 34 | $dependencies = [ 35 | // 'wp-polyfill', 36 | 'wp-api-fetch', 37 | ]; 38 | 39 | $url = plugins_url('/dist', __FILE__); 40 | 41 | // for local development 42 | // when webpack "hot module replacement" is enabled, this 43 | // constant needs to be turned "true" on "wp-config.php" 44 | if (defined('WP_LOCAL_DEV') && WP_LOCAL_DEV) { 45 | $url = 'http://localhost:8080'; 46 | } 47 | 48 | // register scripts 49 | wp_register_script('app-runtime', $url . '/runtime.js', $dependencies, $version, true); 50 | wp_register_script('app-vendors', $url . '/vendors.js', ['app-runtime'], $version, true); 51 | wp_register_script('app-script', $url . '/app.js', [ 'app-vendors' ], $version, true); 52 | 53 | // register styles 54 | wp_register_style('app-css', $url . '/app.css', [ 'wp-components' ], $version); 55 | 56 | // enqueue scripts and styles 57 | wp_enqueue_script('app-script'); 58 | wp_enqueue_style('app-css'); 59 | } 60 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | const config = { 5 | entry: { 6 | app: "./src/index.js", 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, "dist"), 10 | filename: "[name].js", 11 | }, 12 | externals: { 13 | "@wordpress/api-fetch": ["wp", "apiFetch"], 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | use: "babel-loader", 20 | exclude: /node_modules/, 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | MiniCssExtractPlugin.loader, 26 | { 27 | loader: "css-loader", 28 | options: { 29 | importLoaders: 1, 30 | }, 31 | }, 32 | "postcss-loader", 33 | ], 34 | }, 35 | { 36 | test: /\.scss$/, 37 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 38 | }, 39 | { 40 | test: /\.svg$/, 41 | use: "file-loader", 42 | }, 43 | { 44 | test: /\.png$/, 45 | use: [ 46 | { 47 | loader: "url-loader", 48 | options: { 49 | mimetype: "image/png", 50 | }, 51 | }, 52 | ], 53 | }, 54 | ], 55 | }, 56 | resolve: { 57 | extensions: [".js", ".jsx"], 58 | alias: { 59 | "react-dom": "@hot-loader/react-dom", 60 | }, 61 | }, 62 | devServer: { 63 | contentBase: "./dist", 64 | headers: { 65 | "Access-Control-Allow-Origin": "*", 66 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", 67 | "Access-Control-Allow-Headers": 68 | "X-Requested-With, content-type, Authorization", 69 | }, 70 | }, 71 | optimization: { 72 | runtimeChunk: "single", 73 | splitChunks: { 74 | cacheGroups: { 75 | vendor: { 76 | test: /[\\/]node_modules[\\/]/, 77 | name: "vendors", 78 | chunks: "all", 79 | }, 80 | }, 81 | }, 82 | }, 83 | plugins: [new MiniCssExtractPlugin()], 84 | }; 85 | 86 | module.exports = (env, argv) => { 87 | if (argv.hot) { 88 | // Cannot use 'contenthash' when hot reloading is enabled. 89 | config.output.filename = "[name].[hash].js"; 90 | } 91 | 92 | return config; 93 | }; 94 | --------------------------------------------------------------------------------