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

Commit

Permalink
More setState checks (#26)
Browse files Browse the repository at this point in the history
This builds on the discussion from #23 after it was merged. It disallows calling setState in all lifecycle hooks, not just willUnmount.
  • Loading branch information
AmaranthineCodices authored and LPGhatguy committed Feb 17, 2018
1 parent 4b4679c commit 5bddc4a
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 9 deletions.
4 changes: 3 additions & 1 deletion docs/pages/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ end
function TestComponent:willUnmount()
print("We're about to unmount!")
end
```
```

**Note:** If you are calling `setState` within `didMount` or `didUpdate`, make sure that you are not calling `setState` unconditionally. If `setState` is called every time `didMount` or `didUpdate` is called, you will cause a stack overflow error.
25 changes: 17 additions & 8 deletions lib/Component.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ local Component = {}

Component.__index = Component

-- The error message that is thrown when setState is called in the wrong place.
-- This is declared here to avoid really messy indentation.
local INVALID_SETSTATE_MESSAGE = [[
setState cannot be used currently, are you calling setState from any of:
* the willUpdate or willUnmount lifecycle hooks
* the init function
* the render function
* the shouldUpdate function]]

--[[
Create a new Roact stateful component class.
Expand Down Expand Up @@ -106,12 +115,9 @@ end
current state object.
]]
function Component:setState(partialState)
-- State cannot be set in any of the following places:
-- * During the component's init function
-- * During the component's render function
-- * After the component has been unmounted (or is in the process of unmounting, e.g. willUnmount)
-- State cannot be set in any lifecycle hooks.
if not self._canSetState then
error("setState cannot be used currently: are you calling setState from an init, render, or willUnmount function?", 0)
error(INVALID_SETSTATE_MESSAGE, 0)
end

local newState = {}
Expand All @@ -134,7 +140,9 @@ end
reconciliation step.
]]
function Component:_update(newProps, newState)
self._canSetState = false
local willUpdate = self:shouldUpdate(newProps or self.props, newState or self.state)
self._canSetState = true

if willUpdate then
self:_forceUpdate(newProps, newState)
Expand All @@ -147,6 +155,7 @@ end
newProps and newState are optional.
]]
function Component:_forceUpdate(newProps, newState)
self._canSetState = false
if self.willUpdate then
self:willUpdate(newProps or self.props, newState or self.state)
end
Expand All @@ -162,9 +171,7 @@ function Component:_forceUpdate(newProps, newState)
self.state = newState
end

self._canSetState = false
local newChildElement = self:render()
self._canSetState = true

if self._handle._reified ~= nil then
-- We returned an element before, update it.
Expand All @@ -182,6 +189,8 @@ function Component:_forceUpdate(newProps, newState)
)
end

self._canSetState = true

if self.didUpdate then
self:didUpdate(oldProps, oldState)
end
Expand Down Expand Up @@ -209,4 +218,4 @@ function Component:_reify(handle)
end
end

return Component
return Component
63 changes: 63 additions & 0 deletions lib/Component.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ return function()
})
end

function InitComponent:render()
return nil
end

local initElement = Core.createElement(InitComponent)

expect(function()
Expand All @@ -207,6 +211,65 @@ return function()
end).to.throw()
end)

it("should throw when called in shouldUpdate", function()
local TestComponent = Component:extend("TestComponent")

local triggerTest

function TestComponent:init()
triggerTest = function()
self:setState({
a = 1
})
end
end

function TestComponent:render()
return nil
end

function TestComponent:shouldUpdate()
self:setState({
a = 1
})
end

local testElement = Core.createElement(TestComponent)

expect(function()
Reconciler.reify(testElement)
triggerTest()
end).to.throw()
end)

it("should throw when called in willUpdate", function()
local TestComponent = Component:extend("TestComponent")
local forceUpdate

function TestComponent:init()
forceUpdate = function()
self:_forceUpdate()
end
end

function TestComponent:render()
return nil
end

function TestComponent:willUpdate()
self:setState({
a = 1
})
end

local testElement = Core.createElement(TestComponent)

expect(function()
Reconciler.reify(testElement)
forceUpdate()
end).to.throw()
end)

it("should throw when called in willUnmount", function()
local TestComponent = Component:extend("TestComponent")

Expand Down

0 comments on commit 5bddc4a

Please sign in to comment.