Using React.js in Mezzio Application
Ok, in 3 previous JavaScript posts, I posted how to use Vue.js in Mezzio Application. Now, in this post, I will show you how to use React.js in Mezzio Application.
Let’s start with download the mezzio skeleton:
composer create-project mezzio/mezzio-skeleton mezzio-react
I assume next you choose the following options:
- Type of Installation: Modular (3)
- Container: Laminas ServiceManager (3)
- Router: Laminas Router (3)
- Template Engine: Laminas View (3)
Now, we are on the same page!
The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.
We can create middleware for that:
<?php declare(strict_types=1); namespace App\Middleware; use Mezzio\Template\TemplateRendererInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) { (function ($template) { $template->layout = false; })->bindTo($this->template, $this->template)($this->template); } return $handler->handle($request); } }
In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:
<?php // src/App/src/ConfigProvider.php declare(strict_types=1); namespace App; use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; class ConfigProvider { // ... public function getDependencies() : array { return [ 'invokables' => [ // ... ], 'factories' => [ // .. Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class, ], ]; } // ... }
and in the pipeline before DispatchMiddleware:
<?php // config/pipeline.php use App\Middleware\XMLHttpRequestTemplateMiddleware; return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { // ... $app->pipe(XMLHttpRequestTemplateMiddleware::class); $app->pipe(DispatchMiddleware::class); // ... };
We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware
:
<?php declare(strict_types=1); namespace App\Middleware; use Laminas\Diactoros\Response\HtmlResponse; use Mezzio\Template\TemplateRendererInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class NotFoundMiddleware implements MiddlewareInterface { private $template; private $config; public function __construct(TemplateRendererInterface $template, array $config) { $this->template = $template; $this->config = $config; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { return new HtmlResponse( $this->template->render($this->config['mezzio']['error_handler']['template_404']) ); } }
The above middleware need to be registered to ConfigProvider:
<?php // src/App/src/ConfigProvider.php declare(strict_types=1); namespace App; use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; class ConfigProvider { // ... public function getDependencies() : array { return [ 'invokables' => [ // ... ], 'factories' => [ // .. Middleware\NotFoundMiddleware::class => ReflectionBasedAbstractFactory::class, ], ]; } // ... }
And then, add to config/pipeline after DispatchMiddleware
:
<?php // config/pipeline.php use App\Middleware\NotFoundMiddleware; use App\Middleware\XMLHttpRequestTemplateMiddleware; return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { // ... $app->pipe(XMLHttpRequestTemplateMiddleware::class); $app->pipe(DispatchMiddleware::class); $app->pipe(NotFoundMiddleware::class); // ... };
Now, let’s add About and Contact page handlers:
1. About Page
<?php // src/App/src/Handler/AboutPageHandler.php declare(strict_types=1); namespace App\Handler; use Laminas\Diactoros\Response\HtmlResponse; use Mezzio\Template\TemplateRendererInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; class AboutPageHandler implements RequestHandlerInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function handle(ServerRequestInterface $request) : ResponseInterface { return new HtmlResponse($this->template->render('app::about-page')); } }
With templates:
<!-- src/templates/app/about-page.phtml --> <h1>About Me</h1> <p> I'm a web developer. </p>
2. Contact Page
<?php // src/App/src/Handler/ContactPageHandler.php declare(strict_types=1); namespace App\Handler; use Laminas\Diactoros\Response\HtmlResponse; use Mezzio\Template\TemplateRendererInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; class ContactPageHandler implements RequestHandlerInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function handle(ServerRequestInterface $request) : ResponseInterface { return new HtmlResponse($this->template->render('app::contact-page')); } }
With templates:
<!-- src/templates/app/contact-page.phtml --> <h1>Contact Me</h1> <p> You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a> </p>
Handlers Registration
Both AbooutPageHandler
and ContactPageHandler
need to be registered in ConfigProvider
class:
<?php // src/App/src/ConfigProvider.php declare(strict_types=1); namespace App; use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory; class ConfigProvider { // ... public function getDependencies() : array { return [ 'invokables' => [ // ... ], 'factories' => [ // .. Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class, Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class, // ... ], ]; } // ... }
and in the routes:
<?php // config/routes.php return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { // ... $app->get('/about', App\Handler\AboutPageHandler::class, 'about'); $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact'); };
Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.
JS dependencies
We can register js dependencies in the layout by add the following js:
<?php // src/App/templates/layout/default.phtml $this->headLink() ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css') ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css'); $this->inlineScript() ->prependFile('/js/app.js', 'module') ->prependFile('https://unpkg.com/react-router-dom@5.2.0/umd/react-router-dom.js') ->prependFile('https://unpkg.com/html-react-parser@0.13.0/dist/html-react-parser.js') ->prependFile('https://unpkg.com/dompurify@2.0.12/dist/purify.js') ->prependFile('https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js') ->prependFile('https://unpkg.com/react@16.13.1/umd/react.production.min.js') ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js') ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js'); ?>
React dependencies are react, react-dom, html-react-parser, and react-router-dom. The dompurify will be used to purify the js before displaying. For /js/app.js
, that’s our code in public directory to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:
<!-- // src/App/templates/layout/default.phtml --> <body class="app"> <div id="root"> </div> <?=$this->inlineScript()?> </body>
Now, we can define a Page component creator function, eg: createPage(), we can create a js file for it that will be consumed by /js/app.js
under public directory later:
// public/js/create-page.js let createPage = (title) => class Page extends React.Component { constructor(props) { super(props); this.state = { content: ''}; } componentDidMount() { new Promise( (resolve) => { fetch( this.props.location.pathname, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', } } ).then(response => resolve(response.text())); }).then(result => { this.setState({ content : result }); document.title = title; }); } render() { return React.createElement( 'div', { className : "app-content", }, React.createElement( "main", { className : "container" }, HTMLReactParser(DOMPurify.sanitize(this.state.content)) ) ); } } export default createPage;
Above, we use location route path page, and change title after content grabbed based on passed title parameter. We use HTMLReactParser
to generate html for the raw html from the server response.
Now, let create a Navigation, we can create Navigation
component, eg, in /js/Navigation.js
under public directory:
// public/js/Navigation.js const { NavLink } = ReactRouterDOM; const { Component, createElement } = React; class Navigation extends Component { render() { return createElement( 'nav', { className : "navbar navbar-expand-sm navbar-dark bg-dark fixed-top", role: "navigation" }, createElement( 'div', { className: "container" }, createElement( 'div', { className: "navbar-header" }, createElement( 'button', { className: "navbar-toggler", "data-toggle": "collapse", "data-target": "#navbarCollapse", "aria-controls": "#navbarCollapse", "aria-expanded": "false", "aria-label": "Toggle navigation" }, createElement( 'span', { className: "navbar-toggler-icon" } ) ), createElement( NavLink, { to: "/", className: "navbar-brand" }, createElement( 'img', { src: "https://docs.laminas.dev/img/laminas-mezzio-rgb.svg", alt: "Laminas Mezzio", height: 56 } ) ) ), createElement( 'div', { className: "collapse navbar-collapse", id: "navbarCollapse" }, createElement( "ul", { className: "navbar-nav mr-auto" }, createElement( "li", { className: "nav-item" }, createElement(NavLink, { className: 'nav-link', to: "/", exact: true }, "Home") ), createElement( "li", { className: "nav-item" }, createElement(NavLink, { className: 'nav-link', to: "/about", exact: true }, "About") ), createElement( "li", { className: "nav-item" }, createElement(NavLink, { className: 'nav-link', to: "/contact", exact: true }, "Contact") ) ) ) ) ); } } export default Navigation;
Above, we define the navigation, with add “active” class on link selected.
Finally, our /js/app.js
under public directory that consume createPage
function and Navigation
component to be used for routing definition and navigation.
// public/js/app.js import createPage from './create-page.js'; import Navigation from './Navigation.js'; const { BrowserRouter, Switch, Route } = ReactRouterDOM; const Main = () => React.createElement( "main", null, React.createElement( Switch, null, React.createElement( Route, { exact: true, path: "/", component: createPage('Home') } ), React.createElement( Route, { exact: true, path: "/about", component: createPage('About') } ), React.createElement( Route, { exact: true, path: "/contact", component: createPage('Contact') } ), React.createElement( Route, { exact: true, path: "*", component: createPage('404 Page') } ) ) ); const Header = () => React.createElement( 'header', { className: 'app-header' }, React.createElement(Navigation) ); const App = () => React.createElement( "div", null, React.createElement(Header, null), React.createElement(Main, null) ); ReactDOM.render( React.createElement( BrowserRouter, null, React.createElement(App, null) ), document.getElementById('root') ); // https://reactjs.org/docs/react-without-jsx.html // https://www.pluralsight.com/guides/just-plain-react // https://codepen.io/pshrmn/pen/YZXZqM?editors=1010
Now, if we check, we will get SPA working:
That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-react
References:
– https://reactjs.org/docs/react-without-jsx.html
– https://www.pluralsight.com/guides/just-plain-react
– https://codepen.io/pshrmn/pen/YZXZqM?editors=1010
leave a comment