Properties
In Overreact you'll work with props a little differently to how you would typically use props in a React app.
Breaking from tradition
When we looked at the Game Loop, we saw how we avoid parts of the React component lifecycle, in order to get greater control of when game state is updated, and when the DOM is synchronized with that state.
In this guide we'll see how that impacts how we work with props.
When building a React app, we mostly don't need to think about the component lifecycle, and how it synchronizes changes to the DOM. So long as we use state in accordance with its expectation of how it should be used, it holds up its end of the bargain to ensure that components are rerendered when the state changes.
Here's a simple component that contains a single piece of state, a count of how many times a button has been pressed. When the button is clicked, the count is increased by one, which triggers a rerender of the component, thus ensuring the new value is displayed. We can write this code without ever knowing the details of how React does this under the hood.
const MyComponent = () => {
const [count, setCount] = useState(0);
const onClick = () => setCount((count) => count + 1);
return (
<button onClick={onClick}>
Clicked {count} times!
</button>
);
};
However, when we're making a game, we don't want React to rerender our components for us, because we need tighter control over when that happens. With Overreact, you'll use props that are more like refs. The example below is equivalent to the one above:
const MyComponent = () => {
const element = useElement();
const count = useProperty(0);
const onClick = () => count.current += 1;
useRender(() => {
element.setText(`Clicked ${count.current} times!`);
});
return <button ref={element.ref} onClick={onClick} />;
};
Passing properties
Working with properties that behave like refs introduces a problem: How do we pass them to child components?
Let's expand on the example above. Let's imagine we need access to the number of times the user clicked the button further up in our app. If this were a regular React app we'd hoist the state up to the parent component, and likely pass in an onClick
callback.
We could do the equivalent with properties, like so, and it would work:
const Parent = () => {
const count = useProperty(0);
return <MyComponent count={count} />;
};
type Props = {
count: Property<number>;
}
const MyComponent: React.FC<Props> = ({ count }) => {
const onClick = () => count.current += 1;
useRender(() => /* ... */);
return <button ref={element.ref} onClick={onClick} />;
};
However, we can do better. The useProperty
hook not only initializes a new property, it can take an existing property as a parameter, and simply passes it through. This – along with the Prop<T>
type – allows us to create components which can either initialize a property or reuse an existing property that something higher up the component tree also has access to.
type Props = {
count: Prop<number>;
};
const MyComponent: React.FC<Props> = (props) => {
const count = useProperty(props.count || 0);
const onClick = () => count.current += 1;
useRender(() => /* ... */);
return <button ref={element.ref} onClick={onClick} />;
};
This can be particularly powerful, allowing us to create components that can either own their own properties, or use properties owned by others. And even when their own their own properties, they can be initialized by the parent component.
All of the following are valid:
// The component owns the property.
return <MyComponent />;
// The component owns the property, initialized by the parent as '5'.
return <MyComponent count={5} />;
// The parent owns the property.
const count = useProperty(0);
return <MyComponent count={count} />;
Invalidation
Remember when we said properties in Overreact are a lot like refs, objects with a current
property. They are a little more.
They also have an invalidated
property, which is automatically set to true
when a new value is assigned to current
, or the value assigned to current
changes. (This happens thanks to the magic of proxies).
The first example we showed above has a flaw, we aren't checking the invalidated
flag (or clearing it) in the render function. As a result, we're setting the text content of the button every single frame, even when it hasn't changed. Here's a better implementation:
useRender(() => {
// Check that the count value actually changed.
if (count.invalidated) {
element.setText(`Clicked ${count.current} times!`);
// Clear the invalidated flag so that we don't render unnecessarily.
count.invalidated = false;
}
});
Clear invalidation flags
When you clear an invalidated
flag on a property, it does not change immediately. Instead, it is scheduled to change after the current render phase, once all registered render functions have completed. This is necessary since a property may be accessed in multiple render functions, and we wouldn't want the first of those to clear the invalidation flag that a subsequent render function will be checking.
Dynamic properties
Oftentimes, the value of one property is directly related to the value of another property. The useDynamicProperty
hook make this easy to setup, like so:
// Map from an angle (in radians) to a point on a circle with a radius of 10.
const angle = useProperty(45);
const pos = useDynamicProperty(angle, (angle): Position => {
return [
Math.sin(angle.current) * 10,
Math.cos(angle.current) * 10,
];
});
Dynamic properties behave just like regular properties, implementing the same interface. One difference is in how they handle invalidation. A dynamic property inherits the invalidation flag from the property from which it is derived.
Dynamic properties can be derived from other dynamic properties.
Special case: Offset positions
A common use case for dynamic properties is to generate positions that are offset from another position. For example, where the collision box of a player does not have the same dimensions as the bitmap sprites for the player.
Since this is a common pattern, we've provided you with the useOffsetPosition
hook:
const pos1 = useProperty([100, 100]);
// Fixed offset of 20 pixels, in both the x and y axes.
const pos2 = useOffsetPosition(pos1, [20, 20]);
// Dynamic offset, using a property.
const offset = useProperty([10, 10]);
const pos3 = useOffsetPosition(pos1, offset);