Skip to content
This repository has been archived by the owner on Dec 13, 2023. It is now read-only.

Forward ref #307

Merged
merged 12 commits into from
May 25, 2021
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Roact Changelog

## Unreleased Changes
* Introduce forwardRef ([#307](https://github.com/Roblox/roact/pull/307)).
* Fixed a bug where the Roact tree could get into a broken state when processing changes to child instances outside the standard lifecycle.
This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301))
* This change is behind the config value tempFixUpdateChildrenReEntrancy ([#301](https://github.com/Roblox/roact/pull/301))
* Added color schemes for documentation based on user preference ([#290](https://github.com/Roblox/roact/pull/290)).
* Fixed stack trace level when throwing an error in `createReconciler` ([#297](https://github.com/Roblox/roact/pull/297)).
* Optimized the memory usage of 'createSignal' implementation. ([#304](https://github.com/Roblox/roact/pull/304))

## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.0) (November 19th, 2020)
## [1.3.1](https://github.com/Roblox/roact/releases/tag/v1.3.1) (November 19th, 2020)
* Added component name to property validation error message ([#275](https://github.com/Roblox/roact/pull/275))

## [1.3.0](https://github.com/Roblox/roact/releases/tag/v1.3.0) (May 5th, 2020)
Expand Down
57 changes: 57 additions & 0 deletions docs/advanced/bindings-and-refs.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,63 @@ end

Since refs use bindings under the hood, they will be automatically updated whenever the ref changes. This means there's no need to worry about the order in which refs are assigned relative to when properties that use them get set.

### Ref Forwarding
In Roact 1.x, refs can only be applied to host components, _not_ stateful or function components. However, stateful or function components may accept a ref in order to pass it along to an underlying host component. In order to implement this, we wrap the given component with `Roact.forwardRef`.

Suppose we have a styled TextBox component that still needs to accept a ref, so that users of the component can trigger functionality like `TextBox:CaptureFocus()`:

```lua
local function FancyTextBox(props)
return Roact.createElement("TextBox", {
Multiline = true,
PlaceholderText = "Enter your text here",
PlaceholderColor3 = Color3.new(0.4, 0.4, 0.4),
[Roact.Change.Text] = props.onTextChange,
})
end
```

If we were to create an element using the above component, we'd be unable to get a ref to point to the underlying "TextBox" Instance:

```lua
local Form = Roact.Component:extend("Form")
function Form:init()
self.textBoxRef = Roact.createRef()
end

function Form:render()
return React.createElement(FancyTextBox, {
onTextChange = function(value)
print("text value updated to:", value)
end
-- This doesn't actually get assigned to the underlying TextBox!
[Roact.Ref] = self.textBoxRef,
})
end

function Form:didMount()
-- Since self.textBoxRef never gets assigned to a host component, this
-- doesn't work, and in fact will be an attempt to access a nil reference!
self.textBoxRef.current:CaptureFocus()
end
```

In this instance, `FancyTextBox` simply doesn't do anything with the ref passed into it. However, we can easily update it using forwardRef:

```lua
local FancyTextBox = React.forwardRef(function(props, ref)
return Roact.createElement("TextBox", {
Multiline = true,
PlaceholderText = "Enter your text here",
PlaceholderColor3 = Color3.new(0.4, 0.4, 0.4),
[Roact.Change.Text] = props.onTextChange,
[Roact.Ref] = ref,
})
end)
```

With the above change, `FancyTextBox` now accepts a ref and assigns it to the "TextBox" host component that it renders under the hood. Our `Form` implementation will successfully capture focus on `didMount`.

### Function Refs
The original ref API was based on functions instead of objects (and does not use bindings). Its use is not recommended for most cases anymore.

Expand Down
9 changes: 9 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ Creates a new reference object that can be used with [Roact.Ref](#roactref).

---

### Roact.forwardRef
```
Roact.createRef(render: (props: table, ref: Ref) -> RoactElement) -> RoactComponent
```

Creates a new component given a render function that accepts both props and a ref, allowing a ref to be forwarded to an underlying host component via [Roact.Ref](#roactref).

---

### Roact.createContext

!!! success "Added in Roact 1.3.0"
Expand Down
28 changes: 28 additions & 0 deletions src/forwardRef.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
local assign = require(script.Parent.assign)
local None = require(script.Parent.None)
local Ref = require(script.Parent.PropMarkers.Ref)

local config = require(script.Parent.GlobalConfig).get()

local excludeRef = {
[Ref] = None,
}

--[[
Allows forwarding of refs to underlying host components. Accepts a render
callback which accepts props and a ref, and returns an element.
]]
local function forwardRef(render)
if config.typeChecks then
assert(typeof(render) == "function", "Expected arg #1 to be a function")
end

return function(props)
local ref = props[Ref]
local propsWithoutRef = assign({}, props, excludeRef)

return render(propsWithoutRef, ref)
end
end

return forwardRef
Loading