GithubHelp home page GithubHelp logo

roblox / roact Goto Github PK

View Code? Open in Web Editor NEW
554.0 27.0 142.0 1.06 MB

A view management library for Roblox Lua similar to React

Home Page: https://roblox.github.io/roact

License: Apache License 2.0

Lua 99.78% Shell 0.22%
roblox lua ui react

roact's Introduction

This repository is deprecated and no longer maintained.

See react-lua for our currently maintained React in Lua library.

Roact

GitHub Actions Build Status Coveralls Coverage Documentation
A declarative UI library for Roblox Lua inspired by React.
ย 

Installation

Method 1: Model File (Roblox Studio)

  • Download the rbxm model file attached to the latest release from the GitHub releases page.
  • Insert the model into Studio into a place like ReplicatedStorage

Method 2: Filesystem

  • Copy the src directory into your codebase
  • Rename the folder to Roact
  • Use a plugin like Rojo to sync the files into a place

For a detailed guide and examples, check out the official Roact documentation.

local LocalPlayer = game:GetService("Players").LocalPlayer

local Roact = require(Roact)

-- Create our virtual tree describing a full-screen text label.
local tree = Roact.createElement("ScreenGui", {}, {
	Label = Roact.createElement("TextLabel", {
		Text = "Hello, world!",
		Size = UDim2.new(1, 0, 1, 0),
	}),
})

-- Turn our virtual tree into real instances and put them in PlayerGui
Roact.mount(tree, LocalPlayer.PlayerGui, "HelloWorld")

License

Roact is available under the Apache 2.0 license. See LICENSE.txt for details.

roact's People

Contributors

amaranthinecodices avatar chasedig avatar chriscerie avatar cliffchapmanrbx avatar conorgriffin37 avatar crossstarcross avatar dougbankspersonal avatar ghostnaps avatar howmanysmall avatar idiomic avatar jeparlefrancais avatar kampfkarren avatar logandark avatar lpghatguy avatar magimaster avatar malikidreeshasankhan avatar mastermarkus avatar matthargett avatar michaelmcdonnell avatar misteruncloaked avatar nicell avatar nobledraconian avatar ok-nick avatar oltrep avatar psewell avatar quenty avatar rgychiu avatar rimuy avatar yjia2 avatar zotethemighty avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

roact's Issues

createElement should warn if Name or Parent are specified

createElement should give a warning that the Name will be unused if specified in properties - it'll always be overwritten with the key in the children table.

The same should be true for Parent; Roact just overwrites the parent during reification, so setting the Parent property in the element props is a performance hit for no gain.

Document getDerivedStateFromProps

This was merged in #57.

Parts:

  • Changelog
  • Docs website

One thing that's going to be kind of tricky is the versioning of docs. We'll probably want to tag methods with what version they were introduced in, and have version-specific notes as well.

Make all components PureComponents?

We've noticed some very significant performance improvements on Roact code internally at Roblox simply by making all of our components extend PureComponent instead of Component.

Since we're comfortable pushing immutability as the way to represent manage data, I think we'd be comfortable making all components effectively PureComponent objects and deprecating (and then removing) PureComponent.

Expose debug status

Roact should expose whether debug is currently enabled to allow addon libraries to respond to it. Right now _DEBUG_ENABLED is a private value of Core and cannot be accessed outside of Roact.

Documentation should detail install into ReplicatedStorage

All of the examples point to ReplicatedStorage.Roact as the location of the library.

The documentation should clarify that it's the recommended place to point the installer to. The installer should probably also default to targeting ReplicatedStorage instead of nil by default!

Expose reconciliation process

In the React documentation, ReactDOM.render is used multiple times in a single example to introduce reconciliation in a simple way. It also can simplify embedding React inside a non-React UI.

Roact requires that all state comes from within a Roact component. Calling Roact.reify a second time will create a new UI instead of updating the existing one.

Should Roact expose a reconcile API on instance handles returned from reify, to allow for easier embedding?

Suspend event listeners during reconciliation?

A common source of bugs in Roact code that I've seen involves hooking into Changed or using GetPropertyChangedSignal, and calling setState in response.

This pattern is perfectly fine as long as the reconciler doesn't affect the properties you're listening to synchronously. I use this technique in my Roact Windowing example to make the windowing responsive to your current scroll position.

One idea that @AmaranthineCodices had would be to suspend those events while the reconciler is running, eliminating that entire class of bugs.

Are there any cases where you'd want these events? It's only problematic when you end up calling setState directly or indirectly.

Switch installer package to regular rbxmx model

We should just generate 'installation packages' by syncing Roact in Studio with Rojo, and then saving the result as a model.

In the future, Rojo will have a command line tool to rbxmx files, but for now, a manual packaging process makes sense. It's also simpler than an install script, since the 'Run Script' menu has some discoverability issues.

Docs

I'm currently working on porting internal documentation to be useful for public consumption.

Considering:

  • GitHub Pages
  • GitBook

Neither solution is exactly perfect, but I want to get something available soon.

Ability to mark a table as a functional component

I'm trying to make a functional component. This functional component has some options associated with it. Since ModuleScripts can only return one value, I decided to make the element a table and set its __call metamethod to the actual functional component. Roact explodes when you try to reify the component, however, because the type check in Core.isFunctionalElement returns false for tables (as expected). I don't really want to create a stateful component - there's no need to track state, and adding that for the sole purpose of storing options with the component seems somewhat wasteful.

Simple example:

local SomeComponent = {}
SomeComponent.Option = {
    A = 1,
    B = 2,
    C = 3,
}

function SomeComponent.Render(props)
    return Roact.createElement("Frame", {
        BackgroundColor3 = Color3.new(props.Option / 3, 0, 0),
    }
end

setmetatable(SomeComponent, { __call = function(_, ...) return SomeComponent.Render(...) end; })

Trying to reify SomeComponent will produce the following error:

ReplicatedStorage.Roact.Reconciler:191: attempt to call field '_new' (a nil value)
Stack Begin
Script 'ReplicatedStorage.Roact.Reconciler', Line 191 - field _reifyInternal
Script 'ReplicatedStorage.Roact.Reconciler', Line 135 - field _reifyInternal (x2)
Script 'ReplicatedStorage.Roact.Reconciler', Line 90 - field reify
-- stack end omitted; leads into user code

It'd be nice to be able to indicate to Roact that SomeComponent is a functional component, even though it's a table - something like defining a special key ([Roact.FunctionalTable] maybe?) in the table. This would let me keep all the enums and such associated with a component with the component itself.

Performance meta-issue

Roact's primary goal is not performance; it's always going to end up slower than writing UI in Roblox by hand and that's okay. @AmaranthineCodices broke down rough performance in a blog post here and showed that Roact is within an order of magnitude of manual UI in at least one simple case.

That being said, there are some improvements that can be made!

Some ideas that can be fleshed out:

  • Asynchronous rendering like React 16 (#18)
  • Smarter context copying (part of #4)
  • Pooling of elements to reduce GC pressure
  • Smarter update dispatching (this is more of a Roact-Rodux issue)

createRef API like React 16.3

React 16.3+ exposes a new ref API that's intended to be the default instead of the callback refs that both React and Roact have.

I've found that a lot of users new to the React model try to do things like call setState inside refs, or do all sorts of trickery inside of them, when they really should be doing those things in didMount instead.

Switching the recommended API to a createRef-like API would prevent these issues in the future.

Rendering instances that cannot be created from scratch

A thought I had: right now, Roact doesn't let you declaratively render instances that cannot be created solely by setting Lua properties. The main example of this in Roblox is UnionOperations! You can't create these from scratch; they have some hidden attributes to them that you can only replicate by calling Clone on the source UnionOperation. It'd be really cool if Roact could allow you to specify a "source" to use when creating a primitive instance - maybe we can allow instances in createElement?

local element = Roact.createElement(referenceToUnionOperation, {
    Position = Vector3.new(1, 2, 3),
    -- ...
})

When a primitive instance with a component of type instance, instead of doing rbx = Instance.new(component) when reifying it, we could do rbx = component:Clone() instead.

Roact should reify booleans as nil

I was trying to conditionally render Roact components like this:

condition and Roact.createElement(...)

This throws an error:

18:06:27.365 - ReplicatedStorage.Roact.Reconciler:238: Cannot reify invalid Roact element "false"
18:06:27.367 - Stack Begin
18:06:27.369 - Script 'ReplicatedStorage.Roact.Reconciler', Line 238 - field _reifyInternal
18:06:27.369 - Script 'ReplicatedStorage.Roact.Reconciler', Line 373 - field _reconcilePrimitiveChildren
18:06:27.370 - Script 'ReplicatedStorage.Roact.Reconciler', Line 291 - field _reconcile
18:06:27.370 - Script 'ReplicatedStorage.Roact.Component', Line 161 - method _forceUpdate
18:06:27.371 - Script 'ReplicatedStorage.Roact.Component', Line 127 - method _update
18:06:27.371 - Script 'ReplicatedStorage.Roact.Component', Line 114 - method setState
18:06:27.372 - Script 'ReplicatedStorage.RoactMaterial.Components.Checkbox', Line 35 - upvalue method
18:06:27.372 - Script 'ReplicatedStorage.Roact.SingleEventManager', Line 27
18:06:27.373 - Stack End

It's trivial to work around this (condition and Roact.createElement(...) or nil), but it would be nice if Roact would accept booleans and render nothing regardless of the boolean's value. React has something similar - React.Component.render may return a boolean value, which results in nothing being rendered.

New mechanism for global configuration

I've thought a lot about #25 and related issues, and I think that there might be a need for an application-level global configuration that's set once and immutable.

Particularly, I'd like to have settings like:

  • Trace all Roblox mutations (#27)
  • Enable traceback storage on element creation (currently DEBUG_ENABLE)
  • Set the frame budget for async rendering
  • Automatically deep-freeze props and state for debugging

The API I'd be looking for would be something like:

Roact.setGlobalConfig({
    elementTracing = true,
    deepFreezePropsAndState = true
})

Any calls to setGlobalConfig after the first would throw an error.

Implement Fragment API

React added the concept of Fragments, which are elements with multiple children that render into their parent.

I think this would be useful to avoid having junk Roblox instances in the tree, and help flatten some of the gross patterns that have developed with regards to nesting.

New reconciler

The reconciler is starting to get pretty darn big, complicated, and a bit convoluted.

I'd like to take another stab at rewriting it from the ground up with better-defined types, and to be a little bit more robust.

My wishlist for the new reconciler:

Expose GetPropertyChangedSignal declaratively

Right now, you can connect to the Changed event in Roact easily with Roact.Event.Changed, but using GetPropertyChangedSignal(name) on a specific property is much more performant and is much cleaner.

Roact makes that painful right now -- the only way right now is to connect in didMount and disconnect in willUnmount, typical for wrappers around APIs like this.

I think we can make Roact support GetPropertyChangedSignal as a first-class citizen for events.

New Context API

Right now, adding a value to Roact's context API looks like this:

function MyComponent:init()
	self._context.blah = "foo"
end

Since we originally built Roact, the React team designed and implemented an entirely new context API that's a lot nicer. I think we should steal it wholesale, since it's exactly what we recommend people build on top of _context anyways:

local ThemeContext = Roact.createContext("default value")

local function UsingValue()
	return Roact.createElement(ThemeContext.Consumer, {
		render = function(theme)
			return Roact.createElement("TextLabel", {
				Text = theme,
			})
		end,
	})

	-- We could also introduce a helper live we've done for other context-using
	-- libraries in the past:

	return ThemeContext.with(function(theme)
		return Roact.createElement("TextLabel", {
			Text = theme,
		})
	end)
end

local function Root()
	return Roact.createElement(ThemeContext.Provider, {
		value = "overridden value",
	}, {
		Inner = Roact.createElement(UsingValue),
	})
end

Some notable changes from the old context API:

  • No ability to explicitly access the internal context table, only the keys you've created
  • Context values will update when the provider is updated (!!)
  • Not possible to pull context values without also subscribing to their updates

I'm on the fence a bit about the contextType API that React has, but it would be useful for components that need to use context values outside of render.

Wrap lifecycle hooks in NoYield blocks?

This was discussed in Discord, but after I thought about it some more I think it should be given some more thought. Roact will throw very strange, very hard to debug errors if you yield in any lifecycle hook. This originally came up in a bug in roact-material: AmaranthineCodices/roact-material#25

Should NoYield be used to enforce the (currently unstated) expectation that no lifecycle hook (or render, for that matter) yields?

Deprecate (and then remove) `willMount`

More and more as we use Roact I'm realizing that willMount probably shouldn't exist. The question comes up on a day-to-day basis whether to use didMount or willMount, so unless there's some functionality I'm missing we should axe the latter.

It existed in React almost entirely to support server-side rendering, but even there it's being replaced with componentDidServerRender (see this RFC).

Get rid of Core, refactor into several files

I don't think there's any reason to have a giant file called Core.

Instead, I'd like to break pretty much everything in that file into its own file and be more selective about what's exported in the public API, which would probably include explicitly laying out each export and getting rid of the giant apply function.

Make reconciliation errors more obvious

If you assign the wrong data type to a Roblox Instance property, you'll get a nasty error that doesn't tell you anything about where the actual error occurred.

We should wrap any Roblox APIs aggressively in pcall to catch these kinds of issues (at least in development).

Test suite revision

Test coverage is not quite what it should be and it's missing some fairly large pieces.

These tests pre-date the test coverage tooling that we have now!

Include LICENSE file in rbxmx distribution?

With rbxpacker, the distribution system let us include LICENSE as a StringValue object contained within the library. The manual model builds I've pushed up for 1.0.0-pre2 don't have a license included in their XML.

I'm not sure how useful it would be to include that license moving forward because the text of the Apache 2.0 license is kind of large. You really don't want to send 3-4 copies of it to your clients when creating a game using Roact, Rodux, and Roact-Rodux.

More reliable detection of Roact elements

There should be a more reliable way to check if a table was returned from createElement than checking the isElement key of the resultant table. This is largely futureproofing - I want to guarantee a way to find out whether these tables actually came from createElement, with no false-positives (some other API sets isElement to true for its own reasons, for example) or possibility of breakage (I don't think Roact provides any guarantees about the structure of createElement's return value).

This is probably best accomplished via a symbol - instead of checking isElement, I could check [Roact.Element] or something similar; createElement would set both [Roact.Element] and isElement (for backwards compatibility, if nothing else) in the table it returns.

Add getDerivedStateFromProps

React provides a lifecycle hook named componentWillReceiveProps. It is invoked with the next property table, which may or may not be equal to the current one. Crucially, it allows setState to be called within it, allowing components to respond to props with a change in state.

This would be very beneficial to have in Roact. Potential use-cases include:

  • Changing a button's hover state if its position changes
  • Resizing a component (implementing your own SizeFromContents, for example) in response to property changes
  • Fixing Roblox/roact-rodux#2

Asynchronous Rendering

Sometimes, complicated trees result in lots and lots of re-renders.

I'd like to reduce the amount of work that Roact does by batching it and making rendering asynchronous, like React does!

Dan Abramov recently posted a good comment as to why React's setState is asynchronous, a change we'll eventually have to make: facebook/react#11527 (comment)

Introduce setState(fn) variant

In order to implement asynchronous rendering (#18), we need to make sure there's an API in place that mirror's React's setState variant that accepts a function.

Dependent updates work just fine right now, which means this code that works today would not work with asynchronous rendering:

setState(self.state.value + 1)

Instead, we should introduce a new API and encourage users to use it today:

setState(function(props, state)
    return {
        value = state.value + 1
    }
end)

Once work on async rendering begins, we can start catching cases where users call setState multiple times in a row synchronously to try to weed out dependent state mutations.

Improve setState error messages

The current error message ended up being super confusing when it's used for every case where setState is locked.

I'm going to refactor the code to introduce special error messages for each case where setState is locked.

Reconciler: attempt to index field '_instance' (a nil value)

Not a very useful stack trace:

15:34:58.326 - ReplicatedStorage.GraphemeVisualizer.Rodux.NoYield:26: ReplicatedStorage.GraphemeVisualizer.Roact.Reconciler:327: attempt to index field '_instance' (a nil value)
15:34:58.326 - Stack Begin
15:34:58.327 - Script 'ReplicatedStorage.GraphemeVisualizer.Rodux.NoYield', Line 13 - upvalue resultHandler
15:34:58.327 - Script 'ReplicatedStorage.GraphemeVisualizer.Rodux.NoYield', Line 26 - upvalue NoYield
15:34:58.328 - Script 'ReplicatedStorage.GraphemeVisualizer.Rodux.Store', Line 105 - method flush
15:34:58.328 - Script 'ReplicatedStorage.GraphemeVisualizer.Rodux.Store', Line 43
15:34:58.329 - Stack End

Removing Reconciler.teardown's attempt to sever the circular reference between instances and instance handles fixes the issue.

setState can't remove values from state

React is a JavaScript library, which means that it was designed around the existence of both null and undefined.

In React, if you want to remove a value from your state, you can use:

this.setState({
    foo: null
})

In Lua with Roact, if you try the same thing, you'll realize it doesn't do anything:

self:setState({
    foo = nil
})

Lua doesn't have a way to differentiate between "nothing" and "absent" in tables! This is an API hole in Roact!

We need some way to remove a value from the state!

Two potential solutions:

  • Introduce self:removeFromState(key)
    • This means that operations that do both a setState and a removeFromState will trigger two re-renders right now D:
  • Add Symbol value that represents nil
    • There's no case where this value would end up in your state accidentally, but what would we call this symbol? Roact.None?

Opt-in PropTypes implementation

It would be nice to have a feature similar to React's PropTypes functionality.

Overview

Each stateful component may have a key defined on it - with a symbol, ideally, to avoid name collisions - with a table. This table is structured like this:

{
    PropertyKey = "Vector3",
    OtherProperty = "table",
    ThirdProperty = Roact.PropTypes.array,
    -- ...
}

When the stateful component's props table is changed and DEBUG_ENABLE has been called, Roact will check each key in the props table, match it to the corresponding value in the property types table, and error if the types differ.

The property types need not be limited to simple typeof(value) == expectedType expressions. Custom rules that provide more granular type checking allow for checking if, for example, a table is an array (e.g. ThirdProperty in the above example).

Type rules

A "type rule" is the value of an entry in the property types table. It can be one of three possible types:

  • A string denoting the expected type as returned by typeof.
  • An array denoting one or more possible types that the value may be.
  • A function that takes a single argument: the value to validate. This function must return true or false, with an optional second return value explaining why the value was rejected (unused if the function returns true).

Some built-in rules will be provided, which will usually be functions. Usually these functions will return the actual validators themselves, e.g. PropTypes.enumOf("Font") would return a validator function that returns true if and only if the value is an EnumItem from the Font Enum.

Holes

  • Functional components aren't supported. JS allows you to set arbitrary properties of functions; Lua doesn't, so the only way to support functional components would be to call some sort of manual API to validate the props table.
  • Kinda on the fence about whether this should even be a part of Roact or just a separate library. It would be nice to have Roact integrate with this regardless, though.

'Trace mode': Log all Roblox object mutations

We're finding an awesome amount of bugs with Roblox UI objects due to some projects being written using Roact. However, diagnosing the real problems with them is tricky because Roact obfuscates a lot of the actual behavior under the hood (on purpose!)

It would be useful from both a performance and debugging perspective to have a verbose logging mode for Roact to output virtual tree diffs, Roblox object mutations, and other performance notes.

Custom events on components

It'd be useful to create custom events for components that can be used in the same way as primitive events.

local SomeComponent = Roact.Component:extend("SomeComponent")

function SomeComponent:init()
    self.CustomEvent = Roact.createEvent()
end

function SomeComponent:render()
    -- Render the component, somewhere in here fire self.CustomEvent
end

local element = Roact.createElement(SomeComponent, {
    [Roact.Event.CustomEvent] = function(rbx, option)
        print(option)
    end,
})

Thought: Should the event be fired with the element that the custom event was declared on (so in the above example, rbx == element)? If so, how do you get the underlying Roblox object? I'm not sure how best to do this, or if it's even useful - maybe I'm missing something obvious here.

Is it possible to create custom property for element?

Hello i ran into issue, i was trying to make Material Design like button with extra functionality and ripple effect and i had to create custom property that could be specified just like Roblox properties, but it threw error and told me it's not valid Property of rblxGui Element.

QUESTION: Why not accept all Property names that are specified in properties table, why not let mix roblox properties and custom specified properties?

How would i go about making custom property for Button that has more functionality and features than original roblox button?

I already made two components ButtonComponent and RippleEffectComponent but im stuck at trying to figure out how to go about custom properties.

Also it would be nice if the documentation was fixed :), right now it leads to error page.

Instance pooling

One thing we should plan for in a new reconciler is experimenting with pooling of Roblox instances. Since we can keep the instance itself alongside the previous element, we should be able to reconcile from any state to any other state.

One thing that will be tricky is debugging when a stray connection is left from a userland component. Migrating more code to use Roact.Change should seal up the last big case where that would've been a problem -- GetPropertyChangedSignal. Most other cases of manual signal connections are on singletons.

I'm not sure if this was actually a performance issue, but if it works out, it could make Roact more performant in some cases than even the most carefully-optimized, hand-written code.

createElement mutates props when passing in children

I overlooked this when I first implemented createElement, but the passed in props table is mutated if children is also passed in:

roact/lib/Core.lua

Lines 152 to 158 in 394e9fa

if children then
if props[Core.Children] then
warn("props[Children] was defined but was overridden by third parameter to createElement!")
end
props[Core.Children] = children
end

This is fine if we document that Roact takes ownership of your props table when you pass it in, but I think it could potentially cause confusing behavior if a user tries to re-use props and notices children persisting (or warnings popping up).

I don't really want to copy the entire props table on every createElement since it needs to be as light and fast as possible for cheap reconciliation to be a reality.

Peformance guide in docs

We've accumulated a lot of knowledge that's specific to Roact internally about performance.

Many of the things differ from React now, too, so I think it's time to start a Roact-specific performance guide that's mindful to Roblox.

Add naming for functional components

I want to recommend using functional components by default, but debugging with them is a huge pain because they don't have names.

I'd like to introduce a little utility that keeps a map of functional components to their names:

local function MyComponent(props)
    -- ...
end

Roact.setComponentName(MyComponent, "MyComponent")

or maybe even something like

local MyComponent = Roact.FunctionalComponent("MyComponent", function(props)
    -- ...
end)

but then we sort of lose the idea that functional components are just simple functions.

setState locking throws confusing errors from event listeners

The functionality that throws if setState is used in lifecycle hooks is, in its present implementation, causing some issues with certain event listeners. Sometimes, property change listeners can be invoked synchronously during the render - if they call setState within the synchronous part of the handler, they will throw, because the component is still rendering.

Change events are as synchronous as possible

Change events are executed synchronously where possible. The code will execute in the current context up until it yields; the code after the yield will be executed later. As an example, this code (in a Frame):

local frame = script.Parent

frame:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
    print("Immediate")
    spawn(function()
        print("Yielded")
    end)
end)

print("Before")
frame.Position = UDim2.new(0, 1, 0, 0)
print("After")

prints this output:

Before
Immediate
After
Immediate
Yielded (x2)

Crucially, note that the first Immediate is printed between Before and After.

Why this is an issue in Roact

The current setState locking code is very, very simplistic. Each stateful component stores a boolean that determines whether setting state is allowed at the moment. This flag is set to false whenever the component starts re-rendering, and reset to true when the rendering is over.

The problem comes into play when you have a change event that fires synchronously, as in the scenario detailed above, and you call setState inside that change handler. As an example, this code will throw immediately after being run:

local Roact = require(game.ReplicatedStorage.Roact)
local e = Roact.createElement

local DemoComponent = Roact.PureComponent:extend("Demo")

function DemoComponent:init()
	self.state = {
		value = 1
	}
end

function DemoComponent:render()
	return e("ScreenGui", {}, {
		Frame = e("Frame", {
			Position = UDim2.new(0.5, 0, 0.5, 0),
			[Roact.Event.Changed] = function(rbx, property)
				if property ~= "AbsolutePosition" then return end

				self:setState({
					value = math.random()
				})
			end
		})
	})
end

Roact.reify(e(DemoComponent), game.Players.LocalPlayer:WaitForChild("PlayerGui"))

This is the error thrown:

17:28:20.293 - setState cannot be used currently, are you calling setState from any of:
17:28:20.294 - * the willUpdate or willUnmount lifecycle hooks
17:28:20.294 - * the init function
17:28:20.295 - * the render function
17:28:20.295 - * the shouldUpdate function
17:28:20.295 - Stack Begin
17:28:20.296 - Script 'ReplicatedStorage.Roact.Component', Line 128 - method setState
17:28:20.297 - Script 'Players.ProlificLexicon.PlayerScripts.LocalScript', Line 19 - upvalue method
17:28:20.297 - Script 'ReplicatedStorage.Roact.SingleEventManager', Line 27
17:28:20.298 - Stack End

What happens is the event is connected, then the position is immediately changed. This fires the event, which executes synchronously during the render process. The event listener then calls setState, throwing an error, as designed.

Possible resolutions

There are a couple possible ways to fix this in Roact alone, though users of refs will still suffer from this problem in all of them:

  • spawn all event listeners in a new thread, delaying them by a frame and forcing them to wait until the rendering is completed
  • When rendering is started, suspend all event connections and discard all firings
  • When rendering is started, defer execution of event listeners until after rendering is done
  • Asynchronous rendering (#18)

spawn listeners in a new thread

This is the worst of the options overall. It will immediately resolve the problem, but it also prevents Roblox from re-using event threads. It also yields, which is messy.

Suspend listener invocation

This just turns off events completely while the component is rendering. This has obvious implications for data loss.

Defer listener invocation

This postpones event listener invocation until after rendering is completed, when setState can be safely called again. This has a bunch of possible pitfalls, however. The largest one on my mind is: What happens if the information in the event is stale by the time it's re-rendered? For example, if we fire an event, which will pass rbx to the listener, and then do something to make that rbx reference invalid, how do we resolve that?

Asynchronous rendering

This would fix the issue by disconnecting setState from rendering - calling setState with asynchronous rendering would not lead to a synchronous render, it would lead to a render at some point in the future. Calling setState within the render process (as would happen from a synchronously-invoked change listener) would cause another render to happen at some point in the future.

See also

  • #17: Original issue that prompted this behavior to be introduced
  • #23: PR that added this behavior
  • #26: Add-on to #23.

Give useful error messages when calling `setState` in the wrong places

Right now, there are three places where calling setState is a really bad idea:

  • Inside init (the component is not initialized and throws an unrelated error)
  • Inside render (render needs to be pure)
  • After the component is unmounted (aah!)

We should keep track of some sort of state on each component and make sure these setState errors are surfaced well.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.