How do I externally affect Roact components?

Hello! I began fiddling with the Roact library today. My question is: how am I supposed to affect a Roact component externally? Here’s an example of what I mean:

-- requiring modules...
local HealthDisplay = Roact.Component:extend()

-- renders a TextLabel displaying the props.Health
function HealthDisplay:render()
    local health = self.props.Health
    return Roact.createElement("TextLabel", {
        -- properties like Position and Size...
        Text = "Health"..tostring(Health)
    })
end

local guiHandle = Roact.mount(Roact.createElement("ScreenGui", {}, {
    hDisplay = Roact.createElement(HealthDisplay, {Health = 100})
})

humanoid:GetPropertyChangedSignal("Health"):Connect(function()
    -- pseudocode
    guiHandle.ScreenGui.HealthDisplay.Health = humanoid.Health
    -- ^^^here I would like to update the health on the TextLabel
end)

I can’t use state here, because state cannot be accessed from outside of the component, and while I can update the whole handle, that seems messy and slow. I feel like there is something for this that is a lot better than writing my own function to update the whole handle (which would need to be updated often as the humanoid regens).

Explanation on how I should do this is much appreciated.

You can do this with state (more info): State and Lifecycle - Roact Documentation

You can then input other parts of the lifecycle, which in this case would be :init, :didMount and :willUnmount, and connect that to the HealthChanged event.

For example:

function HealthDisplay:init()
    self.HealthChangedConn = nil

	self.state = { -- this must be uncapitalised
		Health = self.props.Health
	}
end

function HealthDisplay:didMount()
	self.HealthChangedConn = humanoid.HealthChanged:Connect(function(newHealth)
                -- this is setting the Health in the state that was created in :init
		self:setState({
			Health = newHealth
		})
	end)
end

You will need to use :willUnmount as well to prevent memory leaks, this is why self.HealthChangedConn was created in :init and set in :didMount:

function HealthDisplay:willUnmount()
	self.HealthChangedConn:Disconnect()
	self.HealthChangedConn = nil
end

Then simply, you just connect it into the render function:

function HealthDisplay:render()
	return Roact.createElement("TextLabel", {
		-- properties like Position and Size...
		Text = "Health"..tostring(self.state.Health) -- instead of using self.props.Health, it now uses the state that was created in :init that updates in :didMount
	})
end

Similarly, you can use Bindings instead of self.state if you wanted.

2 Likes

Amazing response, but I have a few questions:

  1. Would this be possible to achieve with state (not bindings) if the event connection wasn’t inside of the component?
  2. What are the differences between state and bindings?

Thanks
PS. do you know any place (other than the devforum) where I can get help with Roact (like a Discord or something)?

  1. Yes, but this is only done by 3 ways on the top of my head. Using Roact.update on the handle, and then using self.props instead of self.state. You cannot access the state outside of the component, if you want to do this you are going to have to use Rodux and RoactRodux. I recommend this method much more than using Roact.update.

The third way is using a BindableEvent, and you can send the BindableEvent through the props into the component, and listen to whenever that event fires, assuming that the event fires whenever the Health Changes as you desired. Example:

function Test:init()
	self.state = {
		Health = 0;
	};
	
	self.BindableEventConn = nil;
end;

function Test:didMount()
	self.BindableEventConn = self.props.BindableEvent:Connect(function(newValue)
		self:setState({
			Health = newValue;
		});
	end);
end;

function Test:willUnmount()
	self.BindableEventConn:Disconnect();
	self.BindableEventConn = nil;
end;

function Test:render()
	return Roact.createElement("TextLabel", {
		Text = "Health: " .. self.state.Health;
	});
end;
  1. Bindings only change the properties that are subscribed to it, whereas State re-renders the whole element. Additionally, Bindings can be mapped unlike State. Example in :render:
function Test:init()
	self.binding, self.updateBinding = Roact.createBinding(0);
end;

function Test:render()
	return Roact.createElement("TextLabel", {
		Text = self.binding:map(function(value)
			return "Health: " .. tostring(value);
		end);
	});
end;

I do not sorry. While I was learning I relied on the documentation.

Is there also a way to delete/add a component without updating the whole tree, or would I have to update everything (which may be slow)? Also, what’s the difference between

self:setState({...})

and

self.state = {...}

as you used the latter above in your first example?

No, you have to update everything, it shouldn’t be a problem if you are not calling it repeatedly.

self.state is used to initialise the state and self:setState is used to update the state, however self:setState can also be used in the :init. So if you were to call self.state = {} that would change the state, it would not update the component, however self:setState will since it calls a Changed Event.

Oh ok I understand the thing with :setState and self.state.
I can imagine a situation where I have to keep instantiating and deleting new UI elements (like if I needed to have an inventory system with different items inside - items could keep getting dropped and collected), so it really feels like there should be a quick way to add/delete them with Roact.

There is a very good resource on this,

Scroll down to the [PureComponent] header; it has an example of how this issue can be fixed: Reduce Reconciliation - Roact Documentation