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

Commit

Permalink
Forward ref (#307)
Browse files Browse the repository at this point in the history
This closes a gap in compatibility with upstream React. While the current Roact implementation doesn't support refs assigned to non-host components, it also doesn't provide a way for non-host components to forward refs idiomatically as described here: https://reactjs.org/docs/forwarding-refs.html

This change introduces an upstream-compatible forwardRef API and loosely adapts some of the upstream tests as well.

Checklist before submitting:

 Add a test to validate that the ref isn't also provided as a member of props
 Added entry to CHANGELOG.md
 Added/updated relevant tests
 Added/updated documentation
  • Loading branch information
ZoteTheMighty committed May 25, 2021
1 parent dcbeb36 commit 058e3c6
Show file tree
Hide file tree
Showing 7 changed files with 440 additions and 2 deletions.
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

0 comments on commit 058e3c6

Please sign in to comment.