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

Commit

Permalink
Add Context API (#246)
Browse files Browse the repository at this point in the history
* Added Context api

Added createContext, provide, and consume APIs to Roact.

* Remove provide and consume

Removes provide and consume from the new Roact context API, so that createContext returns a Provider and Consumer.

* Update from Code Review comments

-Use fragment instead of oneChild
-Use spies for testing
-Use Component over PureComponent
-Check for duplicates in didUpdate
-nil check disconnect

* Update to use new Internal Context API

Updates the createContext API to internally use the new internal Context API.
  • Loading branch information
psewell authored and ZoteTheMighty committed Jan 21, 2020
1 parent 68e0932 commit d9b7f96
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Roact Changelog

## Unreleased Changes
* Added Contexts, which enables easy handling of items that are provided and consumed throughout the tree.

## [1.2.0](https://github.com/Roblox/roact/releases/tag/v1.2.0) (September 6th, 2019)
* Fixed a bug where derived state was lost when assigning directly to state in init ([#232](https://github.com/Roblox/roact/pull/232/))
Expand Down
101 changes: 101 additions & 0 deletions src/createContext.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
local Symbol = require(script.Parent.Symbol)
local Binding = require(script.Parent.Binding)
local createFragment = require(script.Parent.createFragment)
local Children = require(script.Parent.PropMarkers.Children)
local Component = require(script.Parent.Component)

local function createProvider(context)
local Provider = Component:extend("Provider")

function Provider:init(props)
self.binding, self.updateValue = Binding.create(props.value)

local key = context.key
self:__addContext(key, self.binding)
end

function Provider:didUpdate(prevProps)
if prevProps.value ~= self.props.value then
self.updateValue(self.props.value)
end
end

function Provider:render()
return createFragment(self.props[Children])
end

return Provider
end

local function createConsumer(context)
local Consumer = Component:extend("Consumer")

function Consumer:init(props)
local key = context.key
local binding = self:__getContext(key)

if binding ~= nil then
self.state = {
value = binding:getValue(),
}

-- Update if the Context updated
self.disconnect = Binding.subscribe(binding, function()
self:setState({
value = binding:getValue(),
})
end)
else
-- Fall back to the default value if no Provider exists
self.state = {
value = context.defaultValue,
}
end
end

function Consumer.validateProps(props)
if type(props.render) ~= "function" then
return false, "Consumer expects a `render` function"
else
return true
end
end

function Consumer:render()
return self.props.render(self.state.value)
end

function Consumer:willUnmount()
if self.disconnect ~= nil then
self.disconnect()
end
end

return Consumer
end

local Context = {}
Context.__index = Context

function Context.new(defaultValue)
local self = {
defaultValue = defaultValue,
key = Symbol.named("ContextKey"),
}
setmetatable(self, Context)
return self
end

function Context:__tostring()
return "RoactContext"
end

local function createContext(defaultValue)
local context = Context.new(defaultValue)
return {
Provider = createProvider(context),
Consumer = createConsumer(context),
}
end

return createContext
122 changes: 122 additions & 0 deletions src/createContext.spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
return function()
local createContext = require(script.Parent.createContext)
local createElement = require(script.Parent.createElement)
local NoopRenderer = require(script.Parent.NoopRenderer)
local createReconciler = require(script.Parent.createReconciler)
local createSpy = require(script.Parent.createSpy)

local noopReconciler = createReconciler(NoopRenderer)

it("should return a table", function()
local context = createContext("Test")
expect(context).to.be.ok()
expect(type(context)).to.equal("table")
end)

it("should contain a Provider and a Consumer", function()
local context = createContext("Test")
expect(context.Provider).to.be.ok()
expect(context.Consumer).to.be.ok()
end)

describe("Provider", function()
it("should render its children", function()
local context = createContext("Test")

local Listener = createSpy(function()
return nil
end)

local element = createElement(context.Provider, {
value = "Test",
}, {
Listener = createElement(Listener.value),
})

local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree")
noopReconciler.unmountVirtualTree(tree)

expect(Listener.callCount).to.equal(1)
end)
end)

describe("Consumer", function()
it("should expect a render function", function()
local context = createContext("Test")
local element = createElement(context.Consumer)

expect(function()
noopReconciler.mountVirtualTree(element, nil, "Provide Tree")
end).to.throw()
end)

it("should return the default value if there is no Provider", function()
local valueSpy = createSpy()
local context = createContext("Test")

local element = createElement(context.Consumer, {
render = valueSpy.value,
})

local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree")
noopReconciler.unmountVirtualTree(tree)

valueSpy:assertCalledWith("Test")
end)

it("should pass the value to the render function", function()
local valueSpy = createSpy()
local context = createContext("Test")

local function Listener()
return createElement(context.Consumer, {
render = valueSpy.value,
})
end

local element = createElement(context.Provider, {
value = "NewTest",
}, {
Listener = createElement(Listener),
})

local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree")
noopReconciler.unmountVirtualTree(tree)

valueSpy:assertCalledWith("NewTest")
end)

it("should update when the value updates", function()
local valueSpy = createSpy()
local context = createContext("Test")

local function Listener()
return createElement(context.Consumer, {
render = valueSpy.value,
})
end

local element = createElement(context.Provider, {
value = "NewTest",
}, {
Listener = createElement(Listener),
})

local tree = noopReconciler.mountVirtualTree(element, nil, "Provide Tree")

expect(valueSpy.callCount).to.equal(1)
valueSpy:assertCalledWith("NewTest")

noopReconciler.updateVirtualTree(tree, createElement(context.Provider, {
value = "ThirdTest",
}, {
Listener = createElement(Listener),
}))

expect(valueSpy.callCount).to.equal(3)
valueSpy:assertCalledWith("ThirdTest")

noopReconciler.unmountVirtualTree(tree)
end)
end)
end
1 change: 1 addition & 0 deletions src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local Roact = strict {
createRef = require(script.createRef),
createBinding = Binding.create,
joinBindings = Binding.join,
createContext = require(script.createContext),

Change = require(script.PropMarkers.Change),
Children = require(script.PropMarkers.Children),
Expand Down
1 change: 1 addition & 0 deletions src/init.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ return function()
update = "function",
oneChild = "function",
setGlobalConfig = "function",
createContext = "function",

-- These functions are deprecated and throw warnings!
reify = "function",
Expand Down

0 comments on commit d9b7f96

Please sign in to comment.