From e48b2b2efe4f28e1aadd9aa9f19e9de65173e58f Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Tue, 1 May 2018 14:56:25 -0700
Subject: [PATCH 1/7] Generalize mergeState function, implement defaultProps

---
 lib/Component.lua | 42 +++++++++++++++++++++++++++---------------
 1 file changed, 27 insertions(+), 15 deletions(-)

diff --git a/lib/Component.lua b/lib/Component.lua
index 1a7d3cef..a058987d 100644
--- a/lib/Component.lua
+++ b/lib/Component.lua
@@ -18,22 +18,29 @@ local tick = tick
 
 Component.__index = Component
 
-local function mergeState(currentState, partialState)
-	local newState = {}
+--[[
+	Merge any number of dictionaries into a new dictionary, overwriting keys.
 
-	for key, value in pairs(currentState) do
-		newState[key] = value
-	end
+	If a value of `Core.None` is encountered, the key will be removed instead.
+	This is necessary because Lua doesn't differentiate between a key being
+	missing and a key being set to nil.
+]]
+local function merge(...)
+	local result = {}
 
-	for key, value in pairs(partialState) do
-		if value == Core.None then
-			newState[key] = nil
-		else
-			newState[key] = value
+	for i = 1, select("#", ...) do
+		local entry = select(i, ...)
+
+		for key, value in pairs(entry) do
+			if value == Core.None then
+				result[key] = nil
+			else
+				result[key] = value
+			end
 		end
 	end
 
-	return newState
+	return result
 end
 
 --[[
@@ -73,7 +80,12 @@ function Component:extend(name)
 		-- You can see a list of reasons in invalidSetStateMessages.
 		self._setStateBlockedReason = nil
 
-		self.props = props
+		if class.defaultProps == nil then
+			self.props = props
+		else
+			self.props = merge(class.defaultProps, props)
+		end
+
 		self._context = {}
 
 		-- Shallow copy all context values from our parent element.
@@ -101,7 +113,7 @@ function Component:extend(name)
 			local partialState = class.getDerivedStateFromProps(props, self.state)
 
 			if partialState then
-				self.state = mergeState(self.state, partialState)
+				self.state = merge(self.state, partialState)
 			end
 		end
 
@@ -174,7 +186,7 @@ function Component:setState(partialState)
 		end
 	end
 
-	local newState = mergeState(self.state, partialState)
+	local newState = merge(self.state, partialState)
 	self:_update(self.props, newState)
 end
 
@@ -224,7 +236,7 @@ function Component:_forceUpdate(newProps, newState)
 
 			-- getDerivedStateFromProps can return nil if no changes are necessary.
 			if derivedState ~= nil then
-				newState = mergeState(newState or self.state, derivedState)
+				newState = merge(newState or self.state, derivedState)
 			end
 		end
 	end

From f3d229eb7ef3c073b32ce53210b2726c4f1091cb Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Tue, 1 May 2018 16:55:30 -0700
Subject: [PATCH 2/7] Implement defaultProps test

---
 lib/Component.spec.lua | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/lib/Component.spec.lua b/lib/Component.spec.lua
index a596562a..8eda4369 100644
--- a/lib/Component.spec.lua
+++ b/lib/Component.spec.lua
@@ -216,6 +216,40 @@ return function()
 		Reconciler.teardown(handle)
 	end)
 
+	it("should pull values from defaultProps where appropriate", function()
+		local lastProps
+		local TestComponent = Component:extend("TestComponent")
+
+		TestComponent.defaultProps = {
+			foo = "hello",
+			bar = "world",
+		}
+
+		function TestComponent:render()
+			lastProps = self.props
+			return nil
+		end
+
+		local handle = Reconciler.reify(Core.createElement(TestComponent))
+
+		expect(lastProps).to.be.a("table")
+		expect(lastProps.foo).to.equal("hello")
+		expect(lastProps.bar).to.equal("world")
+
+		Reconciler.teardown(handle)
+
+		lastProps = nil
+		handle = Reconciler.reify(Core.createElement(TestComponent, {
+			foo = 5,
+		}))
+
+		expect(lastProps).to.be.a("table")
+		expect(lastProps.foo).to.equal(5)
+		expect(lastProps.bar).to.equal("world")
+
+		Reconciler.teardown(handle)
+	end)
+
 	describe("setState", function()
 		it("should throw when called in init", function()
 			local InitComponent = Component:extend("InitComponent")

From 39af342f61019fc707b98a314000e837fe8950d1 Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Tue, 1 May 2018 16:57:21 -0700
Subject: [PATCH 3/7] Add additional edge case of 'false' value

---
 lib/Component.spec.lua | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/lib/Component.spec.lua b/lib/Component.spec.lua
index 8eda4369..232ddcbe 100644
--- a/lib/Component.spec.lua
+++ b/lib/Component.spec.lua
@@ -248,6 +248,17 @@ return function()
 		expect(lastProps.bar).to.equal("world")
 
 		Reconciler.teardown(handle)
+
+		lastProps = nil
+		handle = Reconciler.reify(Core.createElement(TestComponent, {
+			bar = false,
+		}))
+
+		expect(lastProps).to.be.a("table")
+		expect(lastProps.foo).to.equal("hello")
+		expect(lastProps.bar).to.equal(false)
+
+		Reconciler.teardown(handle)
 	end)
 
 	describe("setState", function()

From d0aa46dccdd88e95f3dabdc76399384bb728ed4d Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Tue, 1 May 2018 17:00:56 -0700
Subject: [PATCH 4/7] Add entry to CHANGELOG

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7e1c2ea..82111d3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 * Added `Roact.Change` for subscribing to `GetPropertyChangedSignal` ([#51](https://github.com/Roblox/roact/pull/51))
 * Added the static lifecycle method `getDerivedStateFromProps` ([#57](https://github.com/Roblox/roact/pull/57))
 * Allow canceling render by returning nil from setState callback ([#64](https://github.com/Roblox/roact/pull/64))
+* Added `defaultProps` value on stateful components to define values for props that aren't specified ([#79](https://github.com/Roblox/roact/pull/79))
 
 ## 1.0.0 Prerelease 2 (March 22, 2018)
 * Removed `is*Element` methods, this is unlikely to affect anyone ([#50](https://github.com/Roblox/roact/pull/50))

From 3b16a30b9bfc470322165c2e6f5e4a85e6bb6cb6 Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Thu, 3 May 2018 12:24:25 -0700
Subject: [PATCH 5/7] Fall back to defaultProps if an update causes a prop to
 transition to nil

---
 lib/Component.lua      | 19 ++++++++++++++++++-
 lib/Component.spec.lua | 31 +++++++++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/lib/Component.lua b/lib/Component.lua
index b0d5af87..7f8f6c90 100644
--- a/lib/Component.lua
+++ b/lib/Component.lua
@@ -229,7 +229,7 @@ function Component:_forceUpdate(newProps, newState)
 	-- Get the class - getDerivedStateFromProps is static.
 	local class = getmetatable(self)
 
-	-- Only update if newProps are given!
+	-- If newProps are passed, compute derived state and default props
 	if newProps then
 		if class.getDerivedStateFromProps then
 			local derivedState = class.getDerivedStateFromProps(newProps, newState or self.state)
@@ -239,6 +239,23 @@ function Component:_forceUpdate(newProps, newState)
 				newState = merge(newState or self.state, derivedState)
 			end
 		end
+
+		if class.defaultProps then
+			-- We only allocate another prop table if there are props that are
+			-- falling back to their default.
+			local replacementProps
+
+			for key in pairs(class.defaultProps) do
+				if newProps[key] == nil then
+					replacementProps = merge(class.defaultProps, newProps)
+					break
+				end
+			end
+
+			if replacementProps then
+				newProps = replacementProps
+			end
+		end
 	end
 
 	if self.willUpdate then
diff --git a/lib/Component.spec.lua b/lib/Component.spec.lua
index 232ddcbe..9aafe893 100644
--- a/lib/Component.spec.lua
+++ b/lib/Component.spec.lua
@@ -261,6 +261,37 @@ return function()
 		Reconciler.teardown(handle)
 	end)
 
+	it("should fall back to correctly defaultProps after an update", function()
+		local lastProps
+		local TestComponent = Component:extend("TestComponent")
+
+		TestComponent.defaultProps = {
+			foo = "hello",
+			bar = "world",
+		}
+
+		function TestComponent:render()
+			lastProps = self.props
+			return nil
+		end
+
+		local handle = Reconciler.reify(Core.createElement(TestComponent, {
+			foo = "hey"
+		}))
+
+		expect(lastProps).to.be.a("table")
+		expect(lastProps.foo).to.equal("hey")
+		expect(lastProps.bar).to.equal("world")
+
+		handle = Reconciler.reconcile(handle, Core.createElement(TestComponent))
+
+		expect(lastProps).to.be.a("table")
+		expect(lastProps.foo).to.equal("hello")
+		expect(lastProps.bar).to.equal("world")
+
+		Reconciler.teardown(handle)
+	end)
+
 	describe("setState", function()
 		it("should throw when called in init", function()
 			local InitComponent = Component:extend("InitComponent")

From b5e4241c4e4c767097c5009172268b2f5def3006 Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Thu, 3 May 2018 12:24:53 -0700
Subject: [PATCH 6/7] Add light documentation to the API reference

I'll probably expand on this more when propTypes are implemented, since
that's where defaultProps starts to shine the brightest.
---
 docs/api-reference.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/docs/api-reference.md b/docs/api-reference.md
index c8593a32..191b2dc7 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -138,6 +138,13 @@ See [the Portals guide](/advanced/portals.md) for a small tutorial and more deta
 
 ## Component API
 
+### defaultProps
+```
+static defaultProps: Dictionary<any, any>
+```
+
+If `defaultProps` is defined on a stateful component, any props that aren't specified when a component is created will be taken from there.
+
 ### init
 ```
 init(initialProps) -> void

From 449e952ad89fd5dc68cb209ee5187d97bd063266 Mon Sep 17 00:00:00 2001
From: Lucien Greathouse <me@lpghatguy.com>
Date: Thu, 3 May 2018 12:28:18 -0700
Subject: [PATCH 7/7] Fix grammatical typo in spec description

---
 lib/Component.spec.lua | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/Component.spec.lua b/lib/Component.spec.lua
index 9aafe893..69e51f46 100644
--- a/lib/Component.spec.lua
+++ b/lib/Component.spec.lua
@@ -261,7 +261,7 @@ return function()
 		Reconciler.teardown(handle)
 	end)
 
-	it("should fall back to correctly defaultProps after an update", function()
+	it("should fall back to defaultProps correctly after an update", function()
 		local lastProps
 		local TestComponent = Component:extend("TestComponent")