roblox / rodux Goto Github PK
View Code? Open in Web Editor NEWA state management library for Roblox Lua inspired by Redux
Home Page: https://roblox.github.io/rodux
License: Apache License 2.0
A state management library for Roblox Lua inspired by Redux
Home Page: https://roblox.github.io/rodux
License: Apache License 2.0
Lua doesn't have pattern matching, so matching on a bunch of actions gives us a bunch of chained if statements that are kind of painful.
What Lua does have are great dictionaries, so it's possible we should expose a method like this Redux library called 'redux-create-reducer'.
In short:
local reducer = createReducer(initialState, {
[Action.SetUsername] = function(state, action) {
-- do something to create a new state
return state
}
})
Consider the following code:
local Rodux = require(workspace.Rodux)
local testReducer = Rodux.createReducer(
nil,
{
TestStringSet = function(state, action)
print("TestStringSet was called")
return action.string
end
}
)
local reducer = Rodux.combineReducers(
{
testString = testReducer
}
)
local SetTestString = function(str)
return {
type = "TestStringSet",
string = str
}
end
local store = Rodux.Store.new(reducer)
store:dispatch(SetTestString("foo"))
print(store:getState().testString)
When I run this, only nil
is printed. TestStringSet was called
also wasn't printed, which means that the handler wasn't called.
When I change the initialState argument of createReducer
from nil
to any non-nil value ({}
for example"), TestStringSet was called
and foo
are printed, in that order.
At times, it is useful to know what actions were successfully applied to a store (e.g. to fire highly specific listeners that correspond to specific actions). At present, however, the action log is very limited and of fixed length.
It would be useful if there was a way to change the log length, and also clear the log if needed.
Uplift has been publishing versions of Rodux and Roact to the Wally index for Roblox. Some users want updated versions of Rodux, but we're stuck because the Rodux version has not changed and we don't want to publish a conflicting version now.
Here are some options I discussed with some people:
3.0.1
, which will conflict when Roblox officially releases that version (bad! I don't really consider this an option.)3.0.1-ce63e3d
(or similar) which won't conflict with any version Roblox releases, but will require explicitly specifying in wally.toml
since it's a prerelease tag versionHere's # 3! This would be the cleanest solution to give users an up-to-date version of Rodux. A new Release, Tag, or updating rotriever.toml
with a new version is all we need to be confident that we can safely publish that version.
We have some code at Roblox that uses an older version of Rodux (and Roact-Rodux) that has a PascalCase API. Before Rodux went public on GitHub, I changed that API, but that means that we haven't had the latest changes!
Should we introduce a (semi-private) PascalCase compatibility shim to the open source version of the library, or just monkeypatch methods only in the Roblox codebase?
We're going to be shipping stock Lua's optional arguments to debug.traceback
in order to debug coroutines more easily.
Once this is turned on (and stable on all the operating systems we support), we should update Rodux to use those arguments to improve the output of accidental yields.
Right now, Store.changed
only fires once per Heartbeat (or other event, I don't remember) in order to reduce the number of renders resulting from Rodux changes.
When using Rodux standalone, that doesn't make any sense -- your UI framework should be doing the change batching.
I want to implement this render batching in Roact and then remove it from Rodux.
This is a problem in Roact and Roact-Rodux as well.
The contributing guide is the one to axe!
Right now it's unclear how to run the tests or verify the project is working locally.
See #2 for fix.
This is a super common pattern in Redux and Rodux:
local function rootReducer(state, action)
return {
partA = reducerA(state.partA, action),
partB = reducerB(state.partB, action)
}
end
With Redux, this pattern is enabled via combineReducers
. In JavaScript, that looks like this:
const rootReducer = combineReducers({
partA,
partB
})
The method automatically names and combines all of your sub-reducers to create a new reducer, like the one above!
Without the shorthand object syntax ({a, b}
is a list in Lua), the ergonomics will not be as nice I don't think.
Instead of preventing yielding, the Signal implementation should use Bindable events, letting Roblox's task scheduling pick up the slack.
See https://github.com/Quenty/NevermoreEngine/blob/version2/Modules/Events/Signal.lua for an implementation that accepts yielding.
This is a tripup in how middlewares are applied. This ordering of middlewares:
{ middlewareA, middlewareB, middlewareC }
is invoked like this:
dispatch(middlewareA(middlewareB(middlewareC(action))))
in short, they're being applied in reverse order. This is not intuitive behavior; middlewares should be applied in first-in-first-out order, so the middlewares above would be invoked like:
dispatch(middlewareC(middlewareB(middlewareA(action))))
Right now, TestEZ and Lemur are loaded with this awful Lua-shell hybrid script in bin
.
I'd really like to get rid of that script in favor of just using Git submodules.
When I built the first version of Rodux, the best way to respond to changes outside of something like React/Roact wasn't very well understood.
I think that we understand now that the best way to respond to changes in the Rodux store is to keep the last value you had, and on update, compare the new store state with it. In short, you should ignore the second argument of the changed
event!
This issue covers two gripes:
changed
signal because it's a bad practiceonUpdate
maybe?)Currently, in the logger middleware, tables are unnecessarily pretty-printed because of the new Studio expressive output window. Instead, tables should be printed without formatting them beforehand so that developers can access the new output table features (opening/closing tables, etc).
As an alternative, this could be added as an optional configuration when initializing the logger middleware, since the in-game developer console still does not display tables properly.
Things we need to do before this can go live.
Since loggerMiddleware
must be called before using it, there's a potential issue where if you don't call it, it's still a technically valid middleware...just one that silently eats all your actions. I ran into this issue on a personal project. It would be nice if loggerMiddleware
errors if:
Redux has the concept of middleware; it's how redux-thunk is implemented, for example.
We should introduce the same idea to Rodux.
One great concrete example is replicating events to clients and using the Rodux store as a kind of replicated data structure with limited mutation. A blessed middleware that does just this would make Rodux very attractive for managing game state I think
This could lead to memory-leaks, and is bad practice when using rodux.
This probably doesn't come up often in practice, but it seems like a fairly low-effort optimization for this case.
We should introduce a more standardized way of creating and using Rodux actions to prevent common errors and more closely associate actions with the reducers that handle them. A file defining an action would look like this:
-- necessary requires to include Rodux
return Rodux.createAction(script.name, function(value)
return {
value = value,
}
end)
Dispatching that action would look like this:
local MyAction = require(Actions.MyAction)
-- ...
store:Dispatch(MyAction(value))
A reducer that handles this action would look like this:
local MyAction = require(Actions.MyAction)
-- ...
if action.type == MyAction.name then
-- change some state!
end
We should model Rodux.createAction
off of the following Action.lua
implementation:
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
There is still this on the Reducer page
local reducer = function(action, state)
return {
myPhoneNumber = phoneNumberReducer(state.myPhoneNumber, action),
myFriends = friendsReducer(state.myFriends, action),
}
end
Where the action and state parameters are inverted. It's already fixed in the docs it's just not published online yet.
Roact has nice documentation that I've been working on rewriting in MkDocs. Doing the same treatment for Rodux would be excellent, since there is no documentation right now.
I would like the ability to access state from other scripts
It'd be cool to have a middleware that automatically makes all items in the store read-only. It'd be similar to the JS package 'deep-freeze', but probably throw errors on mutations instead of silently ignoring them.
Speed of implementation doesn't really matter here, since it'd only be a development tool.
Implementing this in Lua 5.1 might be tricky. Normally for a read-only table, I'd create a new table with __index
and __newindex
metamethods acting as a proxy, but then you wouldn't be able to iterate (or shallow copy) the table! In 5.2, Lua gained __pairs
and __ipairs
which would've helped here, but alas.
There might be one edge case in this implementation. Way back when I first used Redux, I would keep JS Promise
objects in the store as a way to manage request status. Promise objects have internal mutability, which we might consider to be breaking the Redux paradigm, but because it's not observable outside of attaching a handler to the promise, I don't know if it is. If we wanted to support a use case like that, we'd need to make sure not to deep-freeze some objects, or force Promise objects to be userdata instead.
I've noticed that whenever an error is thrown from a function subscribed to Store.changed
, the execution is stopped and the rest of the functions subscribed to Store.changed
do not get fired. This might be more of an issue with Signal
.
Is there any way I can ensure that the store update flush will continue even if one of the subscription functions throws an error? I don't want my entire game to break because of one error being thrown.
This is a straightforward change: instead of calling NoYield
, we'll instead just create a regular coroutine and let it run, presumably via coroutine.wrap
.
This was a restriction that we wanted to impose on the Lua codebases internally, there's not much reason to enforce it for all Rodux consumers if it's easy to avoid the same bugs without an explicit ban.
It would be nice to have an alternative mapping to Signal.disconnect - namely, Signal.Destroy.
This would make it play nicer with the popular Maid class by Quenty.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.