Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Pass Server Data Directly to React Props

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's do one more React example. Head to the registration page. I just got word that the marketing team wants us to render a "featured product" on the sidebar to entice users to definitely want to sign up. We could build that entirely in PHP and Twig. But let's pretend that this featured product widget will have a bunch of cool, interactive functionality. So we've decided to build it in React.

Rendering the New Component

Back in the tutorial/ directory, which you can get by downloading the course code from this page, you should also have a FeaturedProduct.js. Copy that into assets/components/.

This React component receives a product prop and renders a featured product. It's actually so simple that I wouldn't normally use React... but it'll work perfectly for our example.

import React from 'react';
export default function(props) {
return (
<div>
<div className="component-light product-show p-3 mb-5">
<h5 className="text-center">Featured Product!</h5>
... lines 8 - 21
</div>
</div>
)
}

To render this, let's create a new stimulus controller called, how about, featured-product-react_controller.js. I'll cheat and copy the contents of our made-with-love_controller.js, paste here, and then change the import to FeaturedProduct from FeaturedProduct. Render <FeaturedProduct /> below.

import { Controller } from 'stimulus';
import ReactDOM from 'react-dom';
import React from 'react';
import FeaturedProduct from '../components/FeaturedProduct';
export default class extends Controller {
connect() {
ReactDOM.render(
<FeaturedProduct />,
this.element
)
}
}

Super simple. The interesting part about this React component is that it requires a product prop, which is the data for whatever the featured product is. If we wanted to, we could refactor this component to make an Ajax call to some endpoint that returns the featured product as JSON. That's often how you do things in React or Vue.

But... I don't want to do that. Everything would load faster and my life would be simpler if we could avoid creating that endpoint and making that Ajax call. How can we do that? By preparing the featured product data on the server and passing it directly into our React component on page load. That's easy thanks to the values API.

Adding a Value for the Prop

Check it out: we know that our component requires a product prop, which is an object. So in our Stimulus controller, add a static values set to an object with a product key set to Object. We can pass this product value into the component as a prop: product={this.productValue}.

... lines 1 - 5
export default class extends Controller {
static values = {
product: Object
}
connect() {
ReactDOM.render(
<FeaturedProduct product={this.productValue} />,
this.element
)
}
}

Beautiful. Now open the template for this page, which is templates/registration/register.html.twig. Above the h1, add a new div with a col class... and bind the controller right here: {{ stimulus_controller() }} with featured-product-react.

Give this a second argument so we can pass in the product value. Hmm, this will be an object. For now, let's hardcode some data. We know that our featured product object needs properties like id and name. So let's just start with those. I'll say id: 5 and name set to one of our top-selling products.

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div
class="col-sm-3"
{{ stimulus_controller('featured-product-react', {
product: { id: 5, name: 'Lightly used toothbrush' }
}) }}
></div>
... lines 14 - 30
{% endblock %}

That should be enough to see if things are working. Let's go try it! In the browser, refresh and... it is!. It looks a little broken... only because our product is missing some fields. But this proves that the value is being passed to our React component as a prop!

Passing Serialized Objects to a Prop

But what we really want to do is pass real Product data to this prop. We can do that by serializing a product object into JSON.

Open the controller for this page: src/Controller/RegistrationController.php. At the end of the method, add a new argument so we can query for the featured product: ProductRepository $productRepository.

... lines 1 - 6
use App\Repository\ProductRepository;
... lines 8 - 15
class RegistrationController extends AbstractController
{
... lines 18 - 20
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $authenticator, ProductRepository $productRepository): Response
{
... lines 23 - 54
}

Scroll down to where we render the template... and pass a new variable called featuredProduct set to $productRepository->findFeatured(), which is a custom method that I already created.

... lines 1 - 20
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $authenticator, ProductRepository $productRepository): Response
{
... lines 23 - 49
return $this->render('registration/register.html.twig', [
... line 51
'featuredProduct' => $productRepository->findFeatured(),
]);
}
... lines 55 - 56

Back in the template, let's think. We somehow want to transform that featuredProduct object into JSON so we can pass it to the product value. Open up the Product class: src/Entity/Product.php. I already have Symfony's serializer component installed in my app... and I've already added some serialization groups to this class to control exactly which fields are turned into JSON: that's this product:read group.

... lines 1 - 7
use Symfony\Component\Serializer\Annotation\Groups;
... lines 9 - 13
class Product
{
/**
... lines 17 - 19
* @Groups("product:read")
*/
private $id;
... line 23
/**
... line 25
* @Groups("product:read")
... line 27
*/
private $name;
... line 30
/**
... line 32
* @Groups("product:read")
*/
private $description;
... lines 36 - 228
}

The only missing piece is that... well.. there isn't a way in Twig to use Symfony's serializer to transform an object into JSON. So... I built one. You can see it in src/Twig/SerializerExtension.php. This adds a filter called serialize, which just calls the serializer and passes our data. Fun fact, in Symfony 5.3, this filter will come standard with Symfony: a pull request by my friend Jesse Rushlow has already been accepted. So, you soon won't need to build this.

... lines 1 - 8
class SerializeExtension extends AbstractExtension
{
... lines 11 - 24
public function serialize($data, string $format = 'json', array $context = []): string
{
return $this->serializer->serialize($data, $format, $context);
}
}

Anyways, back in the template, set the product value to featuredProduct pipe serialize(), the format - json and... then any serialization context, which is kind of like serialization options. To serialize the fields in the group I set up, pass groups set to product:read.

... lines 1 - 4
{% block body %}
... lines 6 - 7
<div
class="col-sm-3"
{{ stimulus_controller('featured-product-react', {
product: featuredProduct|serialize('json', { groups: 'product:read'})
}) }}
></div>
... lines 14 - 30
{% endblock %}

We're done! We just passed a Product object directly from our server into a React component as a prop by converting it into JSON.

Let's... ya know... see if it actually works. Refresh and... there it is! Inspect element on the sidebar. Look at that product value attribute! It's lovely! Our Twig serialize filter turned the Product object into JSON... then Stimulus automatically parsed that JSON back into an object. Yep, Stimulus loves frontend frameworks. They're just another tool in your toolkit.

Next, each time we add a Stimulus controller to our app, that controller's code is added to the JavaScript that's downloaded on every page. That makes Stimulus super easy to use: add a data-controller to any page or any Ajax response and it will work.

But how can we make sure our JavaScript isn't getting too big? Let's learn how we can visualize the size of our JavaScript files and some amazing tricks with laziness to make them smaller.

Leave a comment!

2
Login or Register to join the conversation

The import statement on line 4 import FeatureProduct from '../components/FeatureProduct';
Should be <blockquote> FeaturedProduct</blockquote>

Reply

Hey apphancer

Do you mean grammatically? Because I don't see any errors in the code

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works perfectly with Stimulus 3!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.18.5
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}
userVoice