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:
- Would this be possible to achieve with state (not bindings) if the event connection wasn’t inside of the component?
- 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)?
- 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;
- 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