React IntegrationLink to heading


LiveComponent supports React, allowing you to mount React components inside your ViewComponents and update their props from the server. The React component is rendered entirely in the browser, while LiveComponent manages the component's state on the server and forwards props to the React component whenever state changes.React components can be rendered in any part of the Rails view layer - including templates, partials, and view components - by rendering an instance of the LiveComponent::React component. This special component functions just like a regular 'ol live component by attaching a Stimulus controller and allowing the developer to update the props, etc. When the component re-renders, LiveComponent reuses the existing React root, passing the updated props into the React component.

Your first React integrationLink to heading

Let's define a simple React component for demonstration purposes. Note that this component will need to be added to your application's JavaScript build pipeline in some way.
import { useState } from "react";

export const Counter = ({ initValue }: { initValue: number }) => {
  const [value, setValue] = useState(initValue);

  return(
    <>
      <div>{value}</div>
      <button onClick={() => setValue(value + 1)}>
        Increment
      </button>
    </>
  );
};
import { useState } from "react";

export const Counter = ({ initValue }: { initValue: number }) => {
  const [value, setValue] = useState(initValue);

  return(
    <>
      <div>{value}</div>
      <button onClick={() => setValue(value + 1)}>
        Increment
      </button>
    </>
  );
};
We now need to register our React component with LiveComponent:
import { ReactRegistry } from "@camertron/live-component";

ReactRegistry.register_component("Counter", Counter);
import { ReactRegistry } from "@camertron/live-component";

ReactRegistry.register_component("Counter", Counter);
Now that the React component has been registered, we can render it from the Rails view layer using LiveComponent::React. The component argument must be the same string you used to register the component. The rest of the arguments are passed as React props.
<%= render(LiveComponent::React.new(component: "Counter", initValue: 1)) %>
<%= render(LiveComponent::React.new(component: "Counter", initValue: 1)) %>

Updating React propsLink to heading

While LiveComponent::React is implemented as a live component, it doesn't offer the ability to attach a custom Stimulus controller the way regular live components do. Updates therefore have to come from a live component wrapper. Normally this isn't too onerous because the wrapper makes the component friendlier to use in Ruby. For example, instead of rendering a LiveComponent::React, you might render a CounterComponent.To wrap our React component, we'll define a component class and a template. Notice that the wrapper component accepts any and all keyword arguments (i.e. **kwargs), stores a reference to them, and passes them to the React component on render.
class CounterComponent < ViewComponent::Base
  include LiveComponent::Base

  def initialize(**kwargs)
    @kwargs = kwargs
  end
end
class CounterComponent < ViewComponent::Base
  include LiveComponent::Base

  def initialize(**kwargs)
    @kwargs = kwargs
  end
end
And here's the template:
<%= render(LiveComponent::React.new(component: "Counter", **@kwargs)) %>
<%= render(LiveComponent::React.new(component: "Counter", **@kwargs)) %>
With this plumbing in place, it is now possible to define a controller for the CounterComponent and re-render whenever necessary.
import { live, LiveController } from "@camertron/live-component";

type CounterComponentProps = {
  initValue: number;
}

@live("CounterComponent")
export class CounterComponent extends LiveController<CounterComponentProps> {
  do_something() {
    this.render((component) => {
      // Your logic here. When the component re-renders, props will automatically
      // get passed to the React component.
    });
  }
}
import { live, LiveController } from "@camertron/live-component";

type CounterComponentProps = {
  initValue: number;
}

@live("CounterComponent")
export class CounterComponent extends LiveController<CounterComponentProps> {
  do_something() {
    this.render((component) => {
      // Your logic here. When the component re-renders, props will automatically
      // get passed to the React component.
    });
  }
}

Putting it all togetherLink to heading

Here's the full example from above, colocated in one place for easier perusal.
Collapse file tree Expand file tree
  • app
    • components
      • counter_component.html.erb
      • counter_component.rb
      • counter_component.tsx
    • views
      • something
        • index.html.erb
1
import { live, LiveController, ReactRegistry } from "@camertron/live-component";
2
import { useState } from "react";
3
4
export const Counter = ({ initValue }: { initValue: number }) => {
5
  const [value, setValue] = useState(initValue);
6
7
  return(
8
    <>
9
      <div>{value}</div>
10
      <button onClick={() => setValue(value + 1)}>
11
        Increment
12
      </button>
13
    </>
14
  );
15
};
16
17
ReactRegistry.register_component("Counter", Counter);
18
19
type CounterComponentProps = {
20
  initValue: number;
21
}
22
23
@live("CounterComponent")
24
export class CounterComponent extends LiveController<CounterComponentProps> {
25
  do_something() {
26
    this.render((component) => {
27
      // Your logic here. When the component re-renders, props will automatically
28
      // get passed to the React component.
29
    });
30
  }
31
}
1
import { live, LiveController, ReactRegistry } from "@camertron/live-component";
2
import { useState } from "react";
3
4
export const Counter = ({ initValue }: { initValue: number }) => {
5
  const [value, setValue] = useState(initValue);
6
7
  return(
8
    <>
9
      <div>{value}</div>
10
      <button onClick={() => setValue(value + 1)}>
11
        Increment
12
      </button>
13
    </>
14
  );
15
};
16
17
ReactRegistry.register_component("Counter", Counter);
18
19
type CounterComponentProps = {
20
  initValue: number;
21
}
22
23
@live("CounterComponent")
24
export class CounterComponent extends LiveController<CounterComponentProps> {
25
  do_something() {
26
    this.render((component) => {
27
      // Your logic here. When the component re-renders, props will automatically
28
      // get passed to the React component.
29
    });
30
  }
31
}