UI Data
In Rish, UI is a pure function of data. To guarantee a deterministic UI, Rish needs to know exactly when data changes.
Value Types
We strictly enforce using Value Types (structs) for Props and State.
- Safety: Value types are copied by value. This prevents accidental mutation of data that might be shared across different parts of the UI tree.
- Performance: Structs avoid garbage collection allocation overhead.
- Determinism: Deep comparison of value types is reliable; comparing references is not.
Rish provides some useful optimized value types like Element, Children and RishList.
The [RishValueType] Attribute
Adding [RishValueType] to your struct definitions triggers Rishenerator to auto-generate high-performance code for memory management and, crucially, Equality Comparisons.
Every Props and State struct must be flagged with [RishValueType].
Guidelines
When defining your data structures:
- Fields: Use public fields for raw data.
- Properties: Use properties for inferred data (calculated from fields).
- Methods: Use methods for more expensive calculations (>O(1)).
[RishValueType]
public struct FooProps {
public bool flag0;
public bool flag1;
public uint n;
public bool flag => flag0 || flag1;
public ulong GetFactorial() {
ulong result = 1;
for (var i = 1; i <= n; i++)
{
result *= i;
}
return result;
}
}Important
Rishenerator's auto-generated equality checks only look at Fields. Properties are ignored during dirty checking.
Equality Checks
Rish determines if an element needs to re-render by comparing the New Props vs. the Old Props or the New State vs. the Old State. If they are not equal, the element is flagged dirty and re-renders.
You can control how specific fields are compared using attributes:
| Attribute | Behavior | Use Case |
|---|---|---|
[IgnoreComparison] |
Field is ignored during checks. | Data that doesn't affect the visual output. |
[EpsilonComparison] |
Uses UnityEngine.Mathf.Approximately. |
Floating point numbers (float). |
[EqualityOperatorComparison] |
Uses == operator. |
Types with custom operator overloads. |
[EqualsMethodComparison] |
Uses .Equals() method. |
Complex types. |
When no comparison attribute is provided, the default behavior is:
- Reference types: Reference equality.
- Delegates: Ignored.
- Value Types: Low-level binary comparison (fastest) unless a Comparer is found.
Comparers
Rishenerator will produce Comparer methods whenever a fast memory comparison is not possible (for example, if a field should be ignored or should be compared using another Comparer).
In rare cases, you may need manual control over how a specific type is compared and you can define a static (T, T) -> Bool Custom Comparer method flagged with the [Comparer] attribute.
For example, Rish’s Element is a Pointer Value Type (and it just holds an id) and to compare Element Definitions we should call the Equals method of the internal reference type instead:
[Comparer]
private static bool Equals(Element a, Element b)
{
var aSet = a.Valid;
var bSet = b.Valid;
if (aSet ^ bSet)
{
return false;
}
if (!aSet)
{
return true;
}
var aDefinition = a.GetDefinition();
var bDefinition = b.GetDefinition();
var aDisposed = aDefinition == null;
var bDisposed = bDefinition == null;
if (aDisposed || bDisposed)
{
return false;
}
return aDefinition.Equals(bDefinition);
}Reference Types
While we discourage using reference types in Props/State, you often need to interface with existing game systems and they may seem unavoidable.
The Solution: Do not put the mutable object itself in Props. Instead, subscribe to changes and copy the relevant data into your State.
public partial class InventoryPocket : RishElement<InventoryPocketProps, InventoryPocketState>, IMountingListener, IPropsListener {
void IMountingListener.ElementDidMount() {
GameState.PlayerInventory.OnChange += Setup;
}
void IMountingListener.ElementWillUnmount() {
GameState.PlayerInventory.OnChange -= Setup;
}
void IPropsListener.PropsDidChange() {
Setup(GameState.PlayerInventory);
}
void IPropsListener.PropsWillChange() { }
protected override Element Render() => ItemFrame.Create(id: State.itemId, quantity: State.quantity);
private void Setup(PlayerInventory inventory) {
var pocket = inventory.Get(Props.index);
// This triggers the re-render ONLY if the values of this pocket actually changed
SetItemId(pocket.ItemId);
SetQuantity(pocket.Quantity);
}
}Use Caution
If you can't avoid to use a reference type (e.g., a `Texture2D` or a `ScriptableObject` configuration) in Props or State, remember that Rish defaults to **Reference Equality** and if the content of the object changes but the reference stays the same, Rish will not detect the change and will not re-render the element.