-
Notifications
You must be signed in to change notification settings - Fork 143
validateProps #94
validateProps #94
Changes from 16 commits
47350e4
61614d4
567d583
4f6d3ab
136890b
e1332c3
5f93de2
bb62d66
226c546
15c8a78
4f811d7
5802c1f
a59233d
6c8ae89
8e411d9
8f62b6e
2c50417
74990f4
255bc80
4643508
f9d2594
e7afda4
36df87c
ca9cc17
417d568
ca6e364
a600bac
36cad55
8933cf0
8f7d97b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -304,4 +304,26 @@ end | |
``` | ||
|
||
!!! note | ||
`getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. | ||
`getDerivedStateFromProps` is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. | ||
|
||
### validateProps | ||
``` | ||
static validateProps(props) -> success, reason | ||
``` | ||
|
||
Performs property validation. You can use this for type-checking properties; there is a [PropTypes](https://github.com/AmaranthineCodices/rbx-prop-types) library to assist in this. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid calling props 'properties' except in their initial introduction. The (shortened) word is used intentionally to explicitly refer to the React and Roact concept as opposed to the general idea of properties. When we introduce your library, let's say something like "there is a library called PropTypes that can be used to validate props declaratively" |
||
|
||
This function will only be called if the `propertyValidation` configuration option is set to `true`. If this function returns `false`, the error message it returns will be thrown in the output, along with a stack trace pointing to the current element. | ||
|
||
```lua | ||
function MyComponent.validateProps(props) | ||
if props.requiredProperty == nil then | ||
return false, "requiredProperty is required" | ||
end | ||
|
||
return false | ||
end | ||
``` | ||
|
||
!!! note | ||
`validateProps`, like `getDerivedStateFromProps`, is a *static* lifecycle method. It does not have access to `self`, and must be a pure function. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,7 @@ function Component:extend(name) | |
end | ||
|
||
class.__index = class | ||
class._extendTraceback = debug.traceback() | ||
|
||
setmetatable(class, { | ||
__tostring = function(self) | ||
|
@@ -309,6 +310,10 @@ function Component:_forceUpdate(newProps, newState) | |
end | ||
end | ||
|
||
if newProps then | ||
self:_validateProps(newProps) | ||
end | ||
|
||
if self.willUpdate then | ||
self._setStateBlockedReason = "willUpdate" | ||
self:willUpdate(newProps or self.props, newState or self.state) | ||
|
@@ -374,6 +379,8 @@ function Component:_mount(handle) | |
|
||
self._setStateBlockedReason = "render" | ||
|
||
self:_validateProps(self.props) | ||
|
||
local virtualTree | ||
if GlobalConfig.getValue("componentInstrumentation") then | ||
local startTime = tick() | ||
|
@@ -424,4 +431,33 @@ function Component:_unmount() | |
self._handle = nil | ||
end | ||
|
||
--[[ | ||
Performs property validation, if it is enabled. | ||
]] | ||
function Component:_validateProps(props) | ||
if not GlobalConfig.getValue("propertyValidation") then return end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep these types blocks to one statement per line, which I think makes them a little easier to read with how keyword-noisy Lua is: if not GlobalConfig.getValue("propertyValidation") then
return
end |
||
|
||
local validator = self.validateProps | ||
|
||
if validator == nil then return end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we put |
||
if typeof(validator) ~= "function" then | ||
-- Hide as much as possible about error location, since this message | ||
-- occurs from several possible call sites with different stack traces. | ||
-- luacheck: ignore 6 | ||
error(("The value of validateProps must be a function (is a %q). Check the definition of the component extended at:\n%s"):format( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we tweak this error message? "The value of validateProps must be a function, but it is a %s." ..
"\nCheck the component defined at:\n%s" Maybe this is a case to bring in |
||
typeof(validator), | ||
self._extendTraceback), | ||
0) | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this totally necessary? We don't bother doing this with lifecycle methods or things like I can see the argument for doing it here since you're already opting into validation, just wondering what the thought process is since it's not something we do elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of the time when function SomeComponent.validateProps(props)
-- do some validation work
end Instead, it's usually composed from a bunch of smaller validators, and the result of the composition is assigned. When you're using PropTypes it looks something like this: SomeComponent.validateProps = PropTypes.object {
someKey = PropTypes.number,
someOtherKey = PropTypes.string,
} The type check is an extra guard against There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha, that makes sense! |
||
|
||
local success, failureReason = validator(props) | ||
|
||
if not success then | ||
failureReason = failureReason or "<No failure reason was given by the validation function>" | ||
error(("Property validation failed:\n%s\n%s"):format( | ||
failureReason, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should explicitly call |
||
self:getElementTraceback() or Core._defaultSource), 0) | ||
end | ||
end | ||
|
||
return Component |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ return function() | |
local Core = require(script.Parent.Core) | ||
local Reconciler = require(script.Parent.Reconciler) | ||
local Component = require(script.Parent.Component) | ||
local GlobalConfig = require(script.Parent.GlobalConfig) | ||
|
||
it("should be extendable", function() | ||
local MyComponent = Component:extend("The Senate") | ||
|
@@ -292,6 +293,106 @@ return function() | |
Reconciler.unmount(handle) | ||
end) | ||
|
||
describe("validateProps", function() | ||
it("should be called if propertyValidation is set", function() | ||
GlobalConfig.set({ | ||
propertyValidation = true, | ||
}) | ||
|
||
local TestComponent = Component:extend("TestComponent") | ||
local callCount = 0 | ||
|
||
TestComponent.validateProps = function(props) | ||
callCount = callCount + 1 | ||
return true | ||
end | ||
|
||
function TestComponent:render() | ||
return nil | ||
end | ||
|
||
local handle = Reconciler.mount(Core.createElement(TestComponent)) | ||
expect(callCount).to.equal(1) | ||
|
||
handle = Reconciler.reconcile(handle, Core.createElement(TestComponent, { | ||
foo = "bar", | ||
})) | ||
expect(callCount).to.equal(2) | ||
|
||
Reconciler.unmount(handle) | ||
expect(callCount).to.equal(2) | ||
|
||
GlobalConfig.reset() | ||
end) | ||
|
||
it("should throw if the function returns false", function() | ||
GlobalConfig.set({ | ||
propertyValidation = true, | ||
}) | ||
|
||
local TestComponent = Component:extend("TestComponent") | ||
|
||
TestComponent.validateProps = function(props) | ||
return false | ||
end | ||
|
||
function TestComponent:render() | ||
return nil | ||
end | ||
|
||
expect(function() | ||
Reconciler.mount(Core.createElement(TestComponent)) | ||
end).to.throw() | ||
|
||
GlobalConfig.reset() | ||
end) | ||
|
||
it("should throw if validateProps is not a function", function() | ||
GlobalConfig.set({ | ||
propertyValidation = true, | ||
}) | ||
|
||
local TestComponent = Component:extend("TestComponent") | ||
|
||
TestComponent.validateProps = false | ||
|
||
function TestComponent:render() | ||
return nil | ||
end | ||
|
||
expect(function() | ||
Reconciler.mount(Core.createElement(TestComponent)) | ||
end).to.throw() | ||
|
||
GlobalConfig.reset() | ||
end) | ||
|
||
it("should not be run if typeChecking is false", function() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the test name needs to be updated to refer to the config name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, yeah! Fixed now. |
||
local TestComponent = Component:extend("TestComponent") | ||
local callCount = 0 | ||
|
||
TestComponent.validateProps = function(props) | ||
callCount = callCount + 1 | ||
return true | ||
end | ||
|
||
function TestComponent:render() | ||
return nil | ||
end | ||
|
||
local handle = Reconciler.mount(Core.createElement(TestComponent)) | ||
expect(callCount).to.equal(0) | ||
|
||
handle = Reconciler.reconcile(handle, Core.createElement(TestComponent, { | ||
foo = "bar", | ||
})) | ||
expect(callCount).to.equal(0) | ||
|
||
Reconciler.unmount(handle) | ||
expect(callCount).to.equal(0) | ||
end) | ||
end) | ||
|
||
describe("setState", function() | ||
it("should throw when called in init", function() | ||
local InitComponent = Component:extend("InitComponent") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,8 @@ local defaultConfig = { | |
["elementTracing"] = false, | ||
-- Enables instrumentation of shouldUpdate and render methods for Roact components | ||
["componentInstrumentation"] = false, | ||
-- Enables type checking with the validateProps property on stateful components. | ||
["propertyValidation"] = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Much like the recommendation in the docs, let's call this one |
||
} | ||
|
||
-- Build a list of valid configuration values up for debug messages. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,9 @@ Core.None = Symbol.named("None") | |
-- Marker used to specify that the table it is present within is a component. | ||
Core.Element = Symbol.named("Element") | ||
|
||
-- The default "stack traceback" if element tracing is not enabled. | ||
Core._defaultSource = "\n\t<Use Roact.setGlobalConfig with the 'elementTracing' key to enable detailed tracebacks>\n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could probably stand to have a better name, especially now that it's left the scope of the reconciler! Maybe something like I'm starting to feel that we should collect some of the error messages together into a module in a future PR. 🤔 |
||
|
||
--[[ | ||
Utility to retrieve one child out the children passed to a component. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we revert this change in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I...can try; this is an artifact of removing
PropTypes
. I'm not sure I'll be able to without borking the branch though. Submodules are weird >.<