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

Commit

Permalink
Merge branch 'master' into default-props
Browse files Browse the repository at this point in the history
  • Loading branch information
LPGhatguy committed May 3, 2018
2 parents b5e4241 + 3169897 commit b06c73f
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 62 deletions.
13 changes: 13 additions & 0 deletions lib/Change.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
--[[
Change is used to generate special prop keys that can be used to connect to
GetPropertyChangedSignal.
Generally, Change is indexed by a Roblox property name:
Roact.createElement("TextBox", {
[Roact.Change.Text] = function(rbx)
print("The TextBox", rbx, "changed text to", rbx.Text)
end,
})
]]

local Change = {}

local changeMetatable = {
Expand Down
109 changes: 79 additions & 30 deletions lib/Component.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
--[[
The base component implementation that is extended by users of Roact.
The base implementation of a stateful component in Roact.
Exposed as Roact.Component
Stateful components handle most of their own reification and reconciliation
process. Many of the private methods here are invoked by the reconciler.
Stateful components expose a handful of lifecycle events:
- didMount
- willUnmount
- willUpdate
- didUpdate
- (static) getDerivedStateFromProps
These lifecycle events line up with their semantics in React, and more
information (and a diagram) is available in the Roact documentation.
]]

local Reconciler = require(script.Parent.Reconciler)
Expand Down Expand Up @@ -44,12 +55,13 @@ local function merge(...)
end

--[[
Create a new Roact stateful component class.
Create a new stateful component.
Not intended to be a general OO implementation, this function only intends
to let users extend Component and PureComponent.
Instead of using inheritance, use composition and props to extend components.
Instead of using inheritance, use composition and props to extend
components.
]]
function Component:extend(name)
assert(type(name) == "string", "A name must be provided to create a Roact Component")
Expand Down Expand Up @@ -124,8 +136,20 @@ function Component:extend(name)
end

--[[
Override this with a function that returns the elements that should
represent this component with the current state.
render is intended to describe what a UI should look like at the current
point in time.
The default implementation throws an error, since forgetting to define
render is usually a mistake.
The simplest implementation for render is:
function MyComponent:render()
return nil
end
You should explicitly return nil from functions in Lua to avoid edge cases
related to none versus nil.
]]
function Component:render()
local message = (
Expand All @@ -147,7 +171,8 @@ end
written shouldUpdate methods *will* cause hard-to-trace bugs.
If you're thinking of writing a shouldUpdate function, consider using
PureComponent instead, which provides a good implementation.
PureComponent instead, which provides a good implementation given that your
data is immutable.
This function must be faster than the render method in order to be a
performance improvement.
Expand All @@ -157,8 +182,32 @@ function Component:shouldUpdate(newProps, newState)
end

--[[
Applies new state to the component. `partialState` is merged into the
current state object.
Applies new state to the component.
partialState may be one of two things:
- A table, which will be merged onto the current state.
- A function, returning a table to merge onto the current state.
The table variant generally looks like:
self:setState({
foo = "bar",
})
The function variant generally looks like:
self:setState(function(prevState, props)
return {
foo = prevState.count + 1,
})
end)
The function variant may also return nil in the callback, which allows Roact
to cancel updating state and abort the render.
Future versions of Roact will potentially batch or delay state merging, so
any state updates that depend on the current state should use the function
variant.
]]
function Component:setState(partialState)
-- If setState was disabled, we should check for a detailed message and
Expand Down Expand Up @@ -191,7 +240,8 @@ function Component:setState(partialState)
end

--[[
Notifies the component that new props and state are available.
Notifies the component that new props and state are available. This function
is invoked by the reconciler.
If shouldUpdate returns true, this method will trigger a re-render and
reconciliation step.
Expand All @@ -201,13 +251,12 @@ function Component:_update(newProps, newState)

local doUpdate
if GlobalConfig.getValue("componentInstrumentation") then
-- Start timing
local time = tick()
local startTime = tick()

doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state)
-- Finish timing
time = tick() - time
-- Log result
Instrumentation.logShouldUpdate(self._handle, doUpdate, time)

local elapsed = tick() - startTime
Instrumentation.logShouldUpdate(self._handle, doUpdate, elapsed)
else
doUpdate = self:shouldUpdate(newProps or self.props, newState or self.state)
end
Expand All @@ -222,6 +271,8 @@ end
--[[
Forces the component to re-render itself and its children.
This is essentially the inner portion of _update.
newProps and newState are optional.
]]
function Component:_forceUpdate(newProps, newState)
Expand Down Expand Up @@ -279,13 +330,12 @@ function Component:_forceUpdate(newProps, newState)

local newChildElement
if GlobalConfig.getValue("componentInstrumentation") then
-- Start timing
local time = tick()
local startTime = tick()

newChildElement = self:render()
-- End timing
time = tick() - time
-- Log result
Instrumentation.logRenderTime(self._handle, time)

local elapsed = tick() - startTime
Instrumentation.logRenderTime(self._handle, elapsed)
else
newChildElement = self:render()
end
Expand All @@ -294,13 +344,13 @@ function Component:_forceUpdate(newProps, newState)

self._setStateBlockedReason = "reconcile"
if self._handle._reified ~= nil then
-- We returned an element before, update it.
-- We returned an element during our last render, update it.
self._handle._reified = Reconciler._reconcileInternal(
self._handle._reified,
newChildElement
)
elseif newChildElement then
-- We returned nil last time, but not now, so construct a new tree.
-- We returned nil during our last render, construct a new child.
self._handle._reified = Reconciler._reifyInternal(
newChildElement,
self._handle._parent,
Expand All @@ -326,13 +376,12 @@ function Component:_reify(handle)

local virtualTree
if GlobalConfig.getValue("componentInstrumentation") then
-- Start timing
local time = tick()
local startTime = tick()

virtualTree = self:render()
-- End timing
time = tick() - time
-- Log result
Instrumentation.logRenderTime(self._handle, time)

local elapsed = tick() - startTime
Instrumentation.logRenderTime(self._handle, elapsed)
else
virtualTree = self:render()
end
Expand Down
34 changes: 20 additions & 14 deletions lib/Instrumentation.lua
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
--[[
An optional instrumentation layer that the reconciler calls into to record
various events.
Tracks a number of stats, including:
Recorded stats:
- Render count by component
- Update request count by component
- Actual update count by component
- shouldUpdate returned true count by component
- Time taken to run shouldUpdate
- Time taken to render by component
Derivable stats (for profiling manually or with a future tool):
- Average render time by component
- Percent of total render time by component
- Percent of time shouldUpdate returns true
- Average shouldUpdate time by component
- Percent of total shouldUpdate time by component
]]

local Instrumentation = {}

local componentStats = {}
-- Tracks a number of stats, including:
-- Recorded stats:
-- Render count by component
-- Update request count by component
-- Actual update count by component
-- shouldUpdate returned true count by component
-- Time taken to run shouldUpdate
-- Time taken to render by component
-- Derivable stats (for profiling manually or with a future tool):
-- Average render time by component
-- Percent of total render time by component
-- Percent of time shouldUpdate returns true
-- Average shouldUpdate time by component
-- Percent of total shouldUpdate time by component

--[[
Determines name of component from the given instance handle and returns a
Expand Down
5 changes: 3 additions & 2 deletions lib/PureComponent.lua
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
--[[
A version of Component with a `shouldUpdate` method that forces the
resulting component to be pure.
Exposed as Roact.PureComponent
]]

local Component = require(script.Parent.Component)
Expand All @@ -11,9 +9,12 @@ local PureComponent = Component:extend("PureComponent")

-- When extend()ing a component, you don't get an extend method.
-- This is to promote composition over inheritance.
-- PureComponent is an exception to this rule.
PureComponent.extend = Component.extend

function PureComponent:shouldUpdate(newProps, newState)
-- In a vast majority of cases, if state updated, something has updated.
-- We don't bother checking in this case.
if newState ~= self.state then
return true
end
Expand Down
29 changes: 16 additions & 13 deletions lib/Reconciler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,27 @@
The reconciler uses the virtual DOM generated by components to create a real
tree of Roblox instances.
The reonciler has three basic modes of operation:
* reification (public as 'reify')
* reconciliation (private)
* teardown (public)
The reonciler has three basic operations:
* reify
* reconcile
* teardown
Reification is the process of creating new nodes in the tree. This is first
triggered when the user calls `Roact.reify` on a root element. This is where
the structure of the concrete tree is built, later used and modified by the
reconciliation step.
Reification is the process of creating new components. This is first
triggered when the user calls `Roact.reify` on an element. This is where the
structure of the component tree is built, later used and modified by the
reconciliation and teardown steps.
Reconciliation accepts an existing concrete instance tree (created by reify)
along with a new element that describes the desired new state.
The reconciler will do the minimum amount of work required to update the
instances to match the new element, sometimes invoking the reifier to create
new branches.
along with a new element that describes the desired tree. The reconciler
will do the minimum amount of work required to update tree's components to
match the new element, sometimes invoking reify to create new branches.
Teardown is the destructor for the tree. It will crawl through the tree,
destroying nodes in the correct order and invoking lifecycle methods.
destroying nodes from the bottom up.
Much of the reconciler's work is done by Component, which is the base for
all stateful components in Roact. Components can trigger reconciliation (and
implicitly, teardown) via state updates that come with their own caveats.
]]

local Core = require(script.Parent.Core)
Expand Down
7 changes: 4 additions & 3 deletions lib/Symbol.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
--[[
A 'Symbol' is an opaque marker type that can be used to signify unique
statuses. Symbols have the type 'userdata', but when printed to the console,
the name of the symbol is shown.
A 'Symbol' is an opaque marker type.
Symbols have the type 'userdata', but when printed to the console, the name
of the symbol is shown.
]]

local Symbol = {}
Expand Down
6 changes: 6 additions & 0 deletions lib/getDefaultPropertyValue.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
--[[
Attempts to get the default value of a given property on a Roblox instance.
This is used by the reconciler in cases where a prop was previously set on a
primitive component, but is no longer present in a component's new props.
Eventually, Roblox might provide a nicer API to query the default property
of an object without constructing an instance of it.
]]

local Symbol = require(script.Parent.Symbol)
Expand Down

0 comments on commit b06c73f

Please sign in to comment.