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