Giter Site home page Giter Site logo

reactrenderer's Introduction

ReactRenderer

ReactRenderer lets you implement React.js client and server-side rendering in your PHP projects, allowing the development of universal (isomorphic) applications.

It was previously part of ReactBundle but now can be used standalone.

If you wish to use it with Silex, check out @teameh Silex React Renderer Service Provider.

Features include:

  • Prerender server-side React components for SEO, faster page loading, for users that have disabled JavaScript, or for Progressive Web Applications.
  • Twig integration.
  • Client-side render will take the server-side rendered DOM, recognize it, and take control over it without rendering again the component until needed.
  • Error and debug management for server and client side code.
  • Simple integration with Webpack.

Build Status Latest Stable Version Latest Unstable Version License

Complete example

For a complete live example, with a sensible Webpack set up, a sample application to start with and integration in a Symfony Project, check out Symfony React Sandbox.

Installation

ReactRenderer uses Composer, please checkout the composer website in case of doubt about this.

This command will install ReactRenderer into your project.

$ composer require limenius/react-renderer

ReactRenderer follows the PSR-4 convention names for its classes so you can integrate it with your autoloader.

Usage

JavaScript and Webpack set up

In order to use React components you need to register them in your JavaScript. This bundle makes use of the React On Rails npm package to render React Components (don't worry, you don't need to write any Ruby code! ;) ).

Your code exposing a React component would look like this:

import ReactOnRails from "react-on-rails";
import RecipesApp from "./RecipesAppServer";

ReactOnRails.register({ RecipesApp });

Where RecipesApp is the component we want to register in this example.

Note that it is very likely that you will need separated entry points for your server-side and client-side components, for dealing with things like routing. This is a common issue with any universal (isomorphic) application. Again, see the sandbox for an example of how to deal with this.

If you use server-side rendering, you are also expected to have a Webpack bundle for it, containing React, React on Rails and your JavaScript code that will be used to evaluate your component.

Take a look at the Webpack configuration in the symfony-react-sandbox for more information.

Enable Twig Extension

First, you need to configure and enable the Twig extension.

use Limenius\ReactRenderer\Renderer\PhpExecJsReactRenderer;
use Limenius\ReactRenderer\Twig\ReactRenderExtension;
use Limenius\ReactRenderer\Context\SymfonyContextProvider;

// SymfonyContextProvider provides information about the current request, such as hostname and path
// We need an instance of Symfony\Component\HttpFoundation\RequestStack to use it
$contextProvider = new SymfonyContextProvider($requestStack);
$renderer = new PhpExecJsReactRenderer(__DIR__.'/client/build/server-bundle.js', false, $contextProvider);
$ext = new ReactRenderExtension($renderer, $contextProvider, 'both');

$twig->addExtension(new Twig_Extension_StringLoader());
$twig->addExtension($ext);

ReactRenderExtension needs as arguments a renderer and a string that defines if we are rendering our React components client_side, render_side or both.

The renderer is one of the renders that inherit from AbstractReactRenderer.

This library provides currently two renderers:

  • PhpExecJsReactRenderer: that uses internally phpexecjs to autodetect the best javascript runtime available.
  • ExternalServerReactRenderer: that relies on a external nodeJs server.

Now you can insert React components in your Twig templates with:

{{ react_component('RecipesApp', {'props': props}) }}

Where RecipesApp is, in this case, the name of our component, and props are the props for your component. Props can either be a JSON encoded string or an array.

For instance, a controller action that will produce a valid props could be:

/**
 * @Route("/recipes", name="recipes")
 */
public function homeAction(Request $request)
{
    $serializer = $this->get('serializer');
    return $this->render('recipe/home.html.twig', [
        'props' => $serializer->serialize(
            ['recipes' => $this->get('recipe.manager')->findAll()->recipes], 'json')
    ]);
}

Server-side, client-side or both?

You can choose whether your React components will be rendered only client-side, only server-side or both, either in the configuration as stated above or per Twig tag basis.

If you set the option rendering of the Twig call, you can override your config (default is to render both server-side and client-side).

{{ react_component('RecipesApp', {'props': props, 'rendering': 'client_side'}) }}

Will render the component only client-side, whereas the following code

{{ react_component('RecipesApp', {'props': props, 'rendering': 'server_side'}) }}

... will render the component only server-side (and as a result the dynamic components won't work).

Or both (default):

{{ react_component('RecipesApp', {'props': props, 'rendering': 'both'}) }}

You can explore these options by looking at the generated HTML code.

Debugging

One important point when running server-side JavaScript code from PHP is the management of debug messages thrown by console.log. ReactRenderer, inspired React on Rails, has means to replay console.log messages into the JavaScript console of your browser.

To enable tracing, you can set a config parameter, as stated above, or you can set it in your template in this way:

{{ react_component('RecipesApp', {'props': props, 'trace': true}) }}

Note that in this case you will probably see a React warning like

"Warning: render(): Target node has markup rendered by React, but there are unrelated nodes as well. This is most commonly caused by white-space inserted around server-rendered markup."

This warning is harmless and will go away when you disable trace in production. It means that when rendering the component client-side and comparing with the server-side equivalent, React has found extra characters. Those characters are your debug messages, so don't worry about it.

Context

This library will provide context about the current request to React components. Your components will receive two arguments on instantiation:

const App = (initialProps, context) => {};

The Symfony context provider has this implementation:

    public function getContext($serverSide)
    {
        $request = $this->requestStack->getCurrentRequest();

        return [
            'serverSide' => $serverSide,
            'href' => $request->getSchemeAndHttpHost().$request->getRequestUri(),
            'location' => $request->getRequestUri(),
            'scheme' => $request->getScheme(),
            'host' => $request->getHost(),
            'port' => $request->getPort(),
            'base' => $request->getBaseUrl(),
            'pathname' => $request->getPathInfo(),
            'search' => $request->getQueryString(),
        ];
    }

So you can access these properties in your React components, to get information about the request, and if it has been rendered server side or client side.

Server-Side modes

This library supports two modes of using server-side rendering:

  • Using PhpExecJs to auto-detect a JavaScript environment (call node.js via terminal command or use V8Js PHP) and run JavaScript code through it.

  • Using an external node.js server (Example. It will use a dummy server, that knows nothing about your logic to render React for you. Introduces more operational complexity (you have to keep the node server running, which is not a big deal anyways).

Currently, the best option is to use an external server in production, since having V8js is rather hard to compile. However, if you can compile it or your distribution/OS has good packages, it is a very good option if you enable caching, as we will see in the next section.

Redux

If you're using Redux you could use this library to hydrate your store's:

Use redux_store in your Twig file before you render your components depending on your store:

{{ redux_store('MySharedReduxStore', initialState ) }}
{{ react_component('RecipesApp') }}

MySharedReduxStore here is the identifier you're using in your javascript to get the store. The initialState can either be a JSON encoded string or an array.

Then, expose your store in your bundle, just like your exposed your components:

import ReactOnRails from "react-on-rails";
import RecipesApp from "./RecipesAppServer";
import configureStore from "./store/configureStore";

ReactOnRails.registerStore({ configureStore });
ReactOnRails.register({ RecipesApp });

Finally use ReactOnRails.getStore where you would have used your the object you passed into registerStore.

// Get hydrated store
const store = ReactOnRails.getStore("MySharedReduxStore");

return (
  <Provider store={store}>
    <Scorecard />
  </Provider>
);

Make sure you use the same identifier here (MySharedReduxStore) as you used in your Twig file to set up the store.

You have an example in the Sandbox.

Generator Functions

Instead of returning a component, you may choose to return an object from your JavaScript code.

One use case for this is to render Title or other meta tags in Server Side Rendering with React Helmet. You may want to return the generated HTML of the component along with the title.

export default (initialProps, context) => {
  const renderedHtml = {
    componentHtml: renderToString(<MyApp />),
    title: Helmet.renderStatic().title.toString()
  };
  return { renderedHtml };
};

In these cases, the primary HTML code that is going to be rendered must be in the key componentHtml. You can access the resulting array in Twig:

{% set recipes = react_component_array('RecipesApp', {'props': props}) %}
{% block title %}
  {{ recipes.title is defined ? recipes.title | raw : '' }}
{% endblock title %}

{% block body %}
  {{ recipes.componentHtml | raw }}
{% endblock %}

There is an example of this in the sandbox.

Buffering

If you set pass buffered: true as an option of react_component the context and props are not immediately included in the template. All this data is buffered and can be inserted right before the closing body tag with react_flush_buffer:

{{ react_component('RecipesApp', {'props': props, 'buffered': true}) }}
{{ react_flush_buffer() }}

This is recommend if you have a lot of props and don't want to include them in the first parts of your HTML response. See

https://developers.google.com/speed/docs/insights/PrioritizeVisibleContent

(This feature was a flag instead of a named option before 4.0).

Cache

There are two types of cache, of very different nature:

  • You can cache individual components.
  • If you are using the php extension V8 you can cache a snapshot of the V8 virtual machine.

Component cache

Sometimes you want to cache a component and don't go through the server-side rendering process again. In that case, you can set the option cached to true:

{{ react_component('RecipesApp', {'props': props, 'cached': true}) }}

You can also set a cache key. This key could be for instance the id of an entity. Well, it is up to you. If you don't set an id, the id will be based on the name of the component, so no matter what props you pass, the component will be cached and rendered with the same representation.

{{ react_component('RecipesApp', {'props': props, 'cached': true, 'cache_key': "hi_there"}) }}

To enable/disable the cache globally for your app you need to write this configuration. The default value is disabled, so please enable this feature if you plan to use it.

limenius_react:
  serverside_rendering:
    cache:
      enabled: true

(This feature was called previously static render before 4.0).

V8 cache

If in your config.prod.yaml or config/packages/prod/limenius_react.yaml you add the following configuration, and you have V8js installed, this bundle will be much faster:

limenius_react:
  serverside_rendering:
    cache:
      enabled: true
      # name of your app, it is the key of the cache where the v8 snapshot will be stored.
      key: "recipes_app"

After the first page render, this will store a snapshot of the JS virtual machine V8js in the cache, so in subsequent visits, your whole JavaScript app doesn't need to be processed again, just the particular component that you want to render.

With the cache enabled, if you change code of your JS app, you will need to clear the cache.

License

This library is under the MIT license. See the complete license in the bundle:

LICENSE.md

Credits

ReactRenderer is heavily inspired by the great React On Rails, and uses its npm package to render React components.

reactrenderer's People

Contributors

angelopaolosantos avatar check24-larserler avatar deguif avatar garethp avatar gorhawk avatar jenskompter avatar jfoucher avatar jrfnl avatar mykiwi avatar nacmartin avatar ptondereau avatar teameh avatar victoriaq avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reactrenderer's Issues

5.0.4 Release

I would appreciate a new release for the changes in #48

So we can get rid of these php notices when running on php 8.0

PHP Deprecated:  Required parameter $trace follows optional parameter $registeredStores in vendor/limenius/react-renderer/src/Limenius/ReactRenderer/Renderer/PhpExecJsReactRenderer.php on line 94

StaticRenderer in 2.3.0

In 2.3.0 (48b98fb#diff-39b69414ad8ca37f14d248ccb3163359), StaticReactRenderer was introduced as a parameter in ReactRenderer… What does this change/do? We now have to pass an AbstractReactRenderer as well as a SaticreactRenderer (which implements AbstractReactRenderer), why not just keep it to one abstract renderer and keep the decision which one to use (Static, PhpExecJs, …) to whoever uses it?

Usage in PHP without Twig templates?

I'm wondering if it is possible to use this library to perform the React server-side rendering within a standard PHP file without Twig templates? I've spent over a week trying to compile the V8 PHP extension on multiple Ubuntu and CentOS servers and even tried the Docker container with no luck at all, so I've come across this library in search which seems very promising, but I'm looking to use it within a project which just uses standard PHP with no templating engine. Is this possible? I couldn't quite tell from the documentation.

Project dead?

It seems this project is not actively maintained currently.
There are many PRs that are open, awaiting reviews / merges.

@nacmartin if you don't have any time to maintain, would you be open for adding new maintainers?

Dynamically setting cache key causes crash

I'm setting my cache on the fly, because depending on the page I want to have a different cache since my JS files are split per page.

So I do $this->reactRenderer->setCache($this->cacheItemPool, $myDynamicKey);

$this-cacheItemPool is the Symfony cache adapter and the dynamic key consists of the page and another id like string. When using this V8JS just seems to crash, but I can't find any more detailed information besides some invalid OPcode messages in the syslog or short message in the php-fpm logs.

External Server Error: Invalid or unexpected token

Hi!
I've configured the bundle to use an external server; when i try to refresh some page in the browser; i got this Invalid or unexpected token in eval() function while the code in the log is
(function() { var reduxProps, context, storeGenerator, store reduxProps = []; context = { "serverSide":true, "location":"\/app_dev.php\/search\/", "scheme":"http", "port":80, "base":"\/app_dev.php","pathname":"\/search\/", "search":null }; storeGenerator = ReactOnRails.getStoreGenerator('opinionStore'); store = storeGenerator(reduxProps, context); ReactOnRails.setStore('opinionStore', store); var props = []; return ReactOnRails.serverRenderReactComponent({ name: 'mainNode', domNodeId: 'sfreact-reactRenderer5b82edaa320b79.31532393', props: props, trace: true, railsContext: { "serverSide":true, "location":"\/app_dev.php\/search\/", "scheme":"http", "port":80, "base":"\/app_dev.php", "pathname":"\/search\/", "search":null }, }); })()

Service provider for Silex

Just opening this issue to let you know that I finally put some time in open sourcing the Silex service provider I made a while ago to use the renderer in my own projects. It's only a single class, but it will help Silex users to use the renderer easily. You can find it here.

I'll add some tests and documentation soon. I'll probably largely copy and adapt the ReactBundle docs if that's okay with you.

Cheers!
BTW, I changed username: tiemevanveen -> teameh 😏

BC breaks with PHP v5.5 and v5.6

Issue

Changes in recent version of react renderer break backwards compatibility with PHP versions 5.x. The composer.json file references a minimum compatibility with PHP 5.5 (

"php": ">=5.5.0",
)
However there are a number of changes in the repo that do not conform to valid PHP 5 syntax. (Using features that were only added in PHP v7+)

Versions effected

  • 5.*
  • 4.*

How to recreate

Install package in any version of php5 and follow a normal use case

How we might fix it

  • Bump the minimum PHP requirement to 7 in v5
  • Remove all changes that caused BC breaks for php 5 in v4

[Feature Request] Add the ability to statically render in to a twig file

In our development, we've a mixture of entirely react based frontend with some twig templates for static pages. The Twig templates actually end up including static React components for the header and footer, since it shares the header and footer with the front-end. So we've got a static_template.html.twig file that looks like

<!doctype html>
<!-- index.html - used by webpack-dev-server -->
<html>
<head>
    {% block header %}
        <link href="/assets/styles.css" rel="stylesheet" />
    {% endblock %}
</head>
<body>
    {{  react_component('StaticHeader') }}

    {% block content %}
    {% endblock %}

    {{ react_component('StaticFooter') }}
    <script src="/assets/client.js"></script>
</body>
</html>

I was thinking, for speed improvements, it would be nice to have an option to have these actually rendered statically, where it basically rendered the html in to a twig file as a cache (if said file wasn't there) and rendered from that cached twig in the future. That way we can have static components being rendered from react at even less cost than the (admittedly low) cost of hitting up V8JS with a cached context.

Allow split Props for a rendering with "both" settings

hey i'm modifying the Actual ReactRenderer to allow different data pass in props, the reason of why i change it may not be generic so i ask here if i should make a PR or no if those reason are estimated valuable to make one.

Then, Actually i have modified my app to stop using LocalStorage for keep JsonWebToken between two access of the website. Now i use cookie while logging (i create a Symfony user cookie and i send a JWT token too in response) then on the first call of the app i send the JWT from Php to the APP (SSR) BUT on client side i dont wanted send the JWT, i wanted the app make an API call with the cookie to get back the token.
The main reason is, if i pass the token with props with react on rail (on client-side), i get the same security problem as i wanted dodge by switch from local storage to cookie : any javascript app can check the html data to see the "token" (due to react-on-rail pass data from php to react with a plain html for client side)

in fact, what i have changed for ReactRenderer (only for me, not PR yet that's the reason i ask here for may do one) ? only those part of code :

// ... code before ....
public function reactRenderComponentArray($componentName, array $options = array())
    {
  $clientProps = isset($options['clientProps']) ? $options['clientProps'] : null;
  $serverProps = isset($options['serverProps']) ? $options['serverProps'] : null;
  $clientPropsArray = is_array($clientProps) ? $clientProps : json_decode($clientProps);
  $serverPropsArray = is_array($serverProps) ? $serverProps : json_decode($serverProps);
 //... code again ....
 if ($this->shouldRenderClientSide($options)) {
 //.... kepp all exept change the following line of code
 $clientPropsArray != null ?  json_encode($clientPropsArray)  :  json_encode($data['props'])
}

if ($this->shouldRenderServerSide($options)) {
 //.... kepp all exept change the following line of code
$serverPropsArray != null ? json_encode($serverPropsArray) : json_encode($data['props']),
}

}

this will keep old way (passing only value 'props' for both) working and just let any 'serverProps' or 'clientProps' param override the default 'props' if one or both are set :) mean i can pass a serverProps['token'] and a clientProps without the 'token', as serverProps only used in serverSide, all sensitives data i could pass to it are hided in the DOM created with react-on-rail.

this allow this in twig for exemple

// note that serverProps and clientProps are both set by the controller
{% set app_data = react_component_array(appName, {'serverProps': serverProps, 'clientProps' : clientProps, 'rendering': 'both'}) %}
// ... code for SEO etc ...

// rendering the result without the token in the client HTML (cannot be parsed then)

{{ app_data.componentHtml | raw }}

Thx for reading this and tell me if any PR could be required or if this functionality is too specific to be added to this repo as default

PS : one of the reasons i switched from LocalStorage to Cookie is that post https://dev.to/rdegges/please-stop-using-local-storage-1i04
AND in fact, now i can do a proper SSR with cookie (LocalStorage is only readable from client) without pass the JWT to DOM (that would be on opposite of the solution provided by cookie against localstorage)

Using without Symfony?

Is this library usable without twig/symfony - in a sense of a standalone PHP library? If so, how can that be done?

Thanks!

AbstractReactRenderer is not guaranteed to exist

This is regression from #20 .
\Limenius\ReactRenderer\Twig\ReactRenderExtension::__construct() blindly calls \Limenius\ReactRenderer\Renderer\StaticReactRenderer::setRenderer() without checking if the renderer is null. This is an intentionally nullable argument and in certain configurations (e.g. using the bundle with default_rendering: "client_side") will not exist.

Provide railsContext to generator functions

Follow up on #3

See https://github.com/shakacode/react_on_rails/pull/345/files#diff-741e57aaaefab9559b7892657ec8c75eL270

Instead of passing the location we should pass a railsContext now. I'm not sure how this would be implemented best. The quickest way would be to pass a context to our function:

{{ react_component('RecipesApp', {'props': props, 'context': context}) }}

But it would be nicer if the user would not have to do this manually.. Any thoughts?

I would be really nice to have data available in our components. Their version is now providing this as the context:

  {
    # URL settings
    href: request.original_url,
    location: "#{uri.path}#{uri.query.present? ? "?#{uri.query}": ""}",
    scheme: uri.scheme, # http
    host: uri.host, # foo.com
    port: uri.port,
    pathname: uri.path, # /posts
    search: uri.query, # id=30&limit=5

    # Other
    serverSide: boolean # Are we being called on the server or client? NOTE, if you conditionally
     # render something different on the server than the client, then React will only show the
     # server version!
  }

I'm already passing the location as a prop to my components now because we don't had this yet.

railsContext should be passed to the store generator as well btw.

Symantec versioning containing BC breaking changes between 5.0.0 and 5.0.1

Problem

Release versions for Patch and Minor fix (https://semver.org/) level changes have been made with major BC breaks for Twig versions

Example

Between v5.0.0 and v5.0.1 support for twig version 2 and 1 was dropped. By the looks of things this should have been a Major change given it contained changes that broke any backwards compact with older versions of symfony.

Possible fixes

Given v5.01 + have already been released it will be difficult to maintain any updates between versions given it is unclear if version 5.x.x should maintain twig 2 compatibility so securitiy updates / new feature can be added in to the mainline while the current v5.1+ should be moved to v6. Or the we would want to look to maintain compatibility with twig 2 in the v4 set of releases an keep v5 as is ?

Any thoughts @nacmartin @gempir 🤔

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.