1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
local Symbol = require(script.Parent.Symbol)
local createFragment = require(script.Parent.createFragment)
local createSignal = require(script.Parent.createSignal)
local Children = require(script.Parent.PropMarkers.Children)
local Component = require(script.Parent.Component)
--[[
Construct the value that is assigned to Roact's context storage.
]]
local function createContextEntry(currentValue)
return {
value = currentValue,
onUpdate = createSignal(),
}
end
local function createProvider(context)
local Provider = Component:extend("Provider")
function Provider:init(props)
self.contextEntry = createContextEntry(props.value)
self:__addContext(context.key, self.contextEntry)
end
function Provider:willUpdate(nextProps)
-- If the provided value changed, immediately update the context entry.
--
-- During this update, any components that are reachable will receive
-- this updated value at the same time as any props and state updates
-- that are being applied.
if nextProps.value ~= self.props.value then
self.contextEntry.value = nextProps.value
end
end
function Provider:didUpdate(prevProps)
-- If the provided value changed, after we've updated every reachable
-- component, fire a signal to update the rest.
--
-- This signal will notify all context consumers. It's expected that
-- they will compare the last context value they updated with and only
-- trigger an update on themselves if this value is different.
--
-- This codepath will generally only update consumer components that has
-- a component implementing shouldUpdate between them and the provider.
if prevProps.value ~= self.props.value then
self.contextEntry.onUpdate:fire(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.validateProps(props)
if type(props.render) ~= "function" then
return false, "Consumer expects a `render` function"
else
return true
end
end
function Consumer:init(_props)
-- This value may be nil, which indicates that our consumer is not a
-- descendant of a provider for this context item.
self.contextEntry = self:__getContext(context.key)
end
function Consumer:render()
-- Render using the latest available for this context item.
--
-- We don't store this value in state in order to have more fine-grained
-- control over our update behavior.
local value
if self.contextEntry ~= nil then
value = self.contextEntry.value
else
value = context.defaultValue
end
return self.props.render(value)
end
function Consumer:didUpdate()
-- Store the value that we most recently updated with.
--
-- This value is compared in the contextEntry onUpdate hook below.
if self.contextEntry ~= nil then
self.lastValue = self.contextEntry.value
end
end
function Consumer:didMount()
if self.contextEntry ~= nil then
-- When onUpdate is fired, a new value has been made available in
-- this context entry, but we may have already updated in the same
-- update cycle.
--
-- To avoid sending a redundant update, we compare the new value
-- with the last value that we updated with (set in didUpdate) and
-- only update if they differ. This may happen when an update from a
-- provider was blocked by an intermediate component that returned
-- false from shouldUpdate.
self.disconnect = self.contextEntry.onUpdate:subscribe(function(newValue)
if newValue ~= self.lastValue then
-- Trigger a dummy state update.
self:setState({})
end
end)
end
end
function Consumer:willUnmount()
if self.disconnect ~= nil then
self.disconnect()
self.disconnect = nil
end
end
return Consumer
end
local Context = {}
Context.__index = Context
function Context.new(defaultValue)
return setmetatable({
defaultValue = defaultValue,
key = Symbol.named("ContextKey"),
}, Context)
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