├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── default.project.json ├── selene.toml ├── src ├── Context.lua ├── Provider.lua ├── init.lua ├── shallowEqual.lua ├── useCustomDispatch.lua ├── useCustomSelector.lua ├── useDispatch.lua ├── useSelector.lua └── useStore.lua └── wally.toml /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | ServerPackages 3 | roblox.toml 4 | sourcemap.json 5 | wally.lock 6 | *.rbxm 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 2 | 3 | - Fix selectors not properly updating selector function enclosures 4 | 5 | ## 0.3.0 6 | 7 | - Fix `useSelector` and `useCustomSelector` not properly updating when the selector returns a non-table value 8 | 9 | ## 0.2.2 10 | 11 | - Expose `shallowEqual` 12 | 13 | ## 0.2.1 14 | 15 | - Make `equalityFn` param of `useSelector` and `useCustomSelector` optional 16 | 17 | ## 0.2.0 18 | 19 | - Fix useSelector not caching mappedState 20 | - Add useCustomSelector 21 | - Add useCustomDispatch 22 | 23 | ## 0.1.0 24 | 25 | - Initial release 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SolarHorizon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rodux-hooks 2 | 3 | A very simple bridge between Roact and Rodux via [roact-hooks](https://github.com/Kampfkarren/roact-hooks), inspired by [react-redux](https://react-redux.js.org/api/hooks). 4 | 5 | # API 6 | 7 | ## Provider 8 | 9 | Accesses your store via the `store` prop and makes it available to all of its children. Optionally takes a `context` prop if you're using a custom context. 10 | 11 | ```lua 12 | local function app(props, hooks) 13 | return e(RoduxHooks.Provider, { 14 | store = store, 15 | }, { 16 | -- your app goes here 17 | }) 18 | end 19 | ``` 20 | 21 | ## useSelector 22 | 23 | This is the primary way to get data from a Rodux store with rodux-hooks. Multiple useSelector hooks can be used in the same component. 24 | 25 | By default, useSelector will directly compare the previous value returned by `selector` to the most recent one. When the old and new values are not equal, the component will be re-rendered with the updated state. You can optionally pass a function to `equalityFn` for finer control over this. 26 | 27 | ```lua 28 | useSelector( 29 | hooks: RoactHooks, 30 | selector: (state: table), 31 | equalityFn: ((oldState: table, newState: table) -> boolean)? 32 | ) -> any 33 | ``` 34 | 35 | ### Example 36 | 37 | ```lua 38 | local function ExampleLabel(props, hooks) 39 | local money = RoduxHooks.useSelector(hooks, function(state) 40 | return state.money 41 | end) 42 | 43 | return e("TextLabel", { 44 | AnchorPoint = Vector2.new(0.5, 0.5), 45 | Position = UDim2.fromScale(0.5, 0.5), 46 | Size = UDim2.fromOffset(100, 50), 47 | Text = "Money: " .. money, 48 | }) 49 | end 50 | ``` 51 | 52 | ## useDispatch 53 | 54 | Returns the dispatch function of your Rodux store. 55 | 56 | ```lua 57 | useDispatch(hooks: RoactHooks) -> dispatch 58 | ``` 59 | 60 | ```lua 61 | local function ExampleButton(props, hooks) 62 | local dispatch = RoduxHooks.useDispatch(hooks) 63 | 64 | return e("TextButton", { 65 | AnchorPoint = Vector2.new(0.5, 0.5), 66 | Position = UDim2.fromScale(0.5, 0.5), 67 | Size = UDim2.fromOffset(100, 50), 68 | Text = "Click me for free money!", 69 | 70 | [Roact.Event.Activate] = function() 71 | dispatch({ 72 | type = "moneyAdded", 73 | amount = 100, 74 | }) 75 | end, 76 | }) 77 | end 78 | ``` 79 | 80 | ## useStore 81 | 82 | Returns the Rodux store. You will probably rarely ever need to use this. 83 | 84 | ```lua 85 | useStore(hooks: RoactHooks) -> RoduxStore 86 | ``` 87 | 88 | ## shallowEqual 89 | 90 | Does a shallow comparison of two values (usually tables). This is included as a helper function to be used with useSelector. 91 | 92 | ```lua 93 | shallowEqual(x: any, y: any) -> boolean 94 | ``` 95 | 96 | # Custom Context API 97 | 98 | These functions are exposed in the event that you are using a custom context. You would probably only need to do this if you were using more than one store, which is generally frowned upon in Rodux. 99 | 100 | ## useCustomSelector 101 | 102 | Like useSelector, but accepts a custom context. 103 | 104 | ```lua 105 | useCustomSelector( 106 | hooks: RoactHooks, 107 | selector: (state: table), 108 | equalityFn: ((oldState: table, newState: table) -> boolean)?, 109 | customContext: RoactContext 110 | ) -> any 111 | ``` 112 | 113 | ## useCustomDispatch 114 | 115 | Like useDispatch, but accepts a custom context. 116 | 117 | ```lua 118 | useCustomDispatch( 119 | hooks: RoactHooks, 120 | customContext: RoactContext 121 | ) -> dispatch 122 | ``` 123 | 124 | ## Example 125 | 126 | ```lua 127 | local CustomContext = Roact.createContext() 128 | 129 | local function app(props, hooks) 130 | return e(RoduxHooks.Provider, { 131 | store = store, 132 | context = CustomContext, 133 | }, { 134 | -- a bunch of components 135 | }) 136 | end 137 | 138 | -- A component that is a descendant of `app` 139 | local function YetAnotherExample(props, hooks) 140 | local value = RoduxHooks.useCustomSelector(hooks, selector, equalityFn, CustomContext) 141 | local dispatch = RoduxHooks.useCustomDispatch(hooks, CustomContext) 142 | 143 | -- your component here 144 | end 145 | ``` 146 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rodux-hooks", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /src/Context.lua: -------------------------------------------------------------------------------- 1 | local Roact = require(script.Parent.Parent.Roact) 2 | 3 | local Context = Roact.createContext() 4 | 5 | return Context 6 | -------------------------------------------------------------------------------- /src/Provider.lua: -------------------------------------------------------------------------------- 1 | local Roact = require(script.Parent.Parent.Roact) 2 | local Context = require(script.Parent.Context) 3 | 4 | local e = Roact.createElement 5 | 6 | local function Provider(props) 7 | local context = props.context or Context 8 | 9 | return e(context.Provider, { 10 | value = props.store, 11 | }, props[Roact.Children]) 12 | end 13 | 14 | return Provider 15 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | local Provider = require(script.Provider) 2 | local useDispatch = require(script.useDispatch) 3 | local useSelector = require(script.useSelector) 4 | local useStore = require(script.useStore) 5 | local useCustomDispatch = require(script.useCustomDispatch) 6 | local useCustomSelector = require(script.useCustomSelector) 7 | local shallowEqual = require(script.shallowEqual) 8 | 9 | return { 10 | Provider = Provider, 11 | useDispatch = useDispatch, 12 | useSelector = useSelector, 13 | useStore = useStore, 14 | useCustomDispatch = useCustomDispatch, 15 | useCustomSelector = useCustomSelector, 16 | shallowEqual = shallowEqual, 17 | } 18 | -------------------------------------------------------------------------------- /src/shallowEqual.lua: -------------------------------------------------------------------------------- 1 | local function shallowEqual(x, y) 2 | if typeof(x) ~= "table" or typeof(y) ~= "table" then 3 | return x == y 4 | end 5 | 6 | for k, v in pairs(x) do 7 | if y[k] ~= v then 8 | return false 9 | end 10 | end 11 | 12 | for k, v in pairs(y) do 13 | if x[k] ~= v then 14 | return false 15 | end 16 | end 17 | 18 | return true 19 | end 20 | 21 | return shallowEqual 22 | -------------------------------------------------------------------------------- /src/useCustomDispatch.lua: -------------------------------------------------------------------------------- 1 | local function useCustomDispatch(hooks, context) 2 | local store = hooks.useContext(context) 3 | 4 | return function(action) 5 | store:dispatch(action) 6 | end 7 | end 8 | 9 | return useCustomDispatch 10 | -------------------------------------------------------------------------------- /src/useCustomSelector.lua: -------------------------------------------------------------------------------- 1 | local function defaultEqualityFn(newState, oldState) 2 | return newState == oldState 3 | end 4 | 5 | local function useCustomSelector( 6 | hooks, 7 | selector: (state: any) -> any, 8 | equalityFn: ((newState: any, oldState: any) -> boolean)?, 9 | context 10 | ) 11 | -- This value wrapper is required so the variable context of the selector function can be updated on each run -- 12 | local selectorFunc = hooks.useValue() 13 | selectorFunc.value = selector 14 | 15 | local store = hooks.useContext(context) 16 | local mappedState, setMappedState = hooks.useState(function() 17 | return selector(store:getState()) 18 | end) 19 | local oldMappedState = hooks.useValue(mappedState) 20 | 21 | if equalityFn == nil then 22 | equalityFn = defaultEqualityFn 23 | end 24 | 25 | hooks.useEffect(function() 26 | local storeChanged = store.changed:connect(function(newState, _oldState) 27 | local newMappedState = selectorFunc.value(newState) 28 | 29 | if not equalityFn(newMappedState, oldMappedState.value) then 30 | oldMappedState.value = newMappedState 31 | setMappedState(newMappedState) 32 | end 33 | end) 34 | 35 | return function() 36 | storeChanged:disconnect() 37 | end 38 | end, {}) 39 | 40 | return mappedState 41 | end 42 | 43 | return useCustomSelector 44 | -------------------------------------------------------------------------------- /src/useDispatch.lua: -------------------------------------------------------------------------------- 1 | local Context = require(script.Parent.Context) 2 | local useCustomDispatch = require(script.Parent.useCustomDispatch) 3 | 4 | local function useDispatch(hooks) 5 | return useCustomDispatch(hooks, Context) 6 | end 7 | 8 | return useDispatch 9 | -------------------------------------------------------------------------------- /src/useSelector.lua: -------------------------------------------------------------------------------- 1 | local Context = require(script.Parent.Context) 2 | local useCustomSelector = require(script.Parent.useCustomSelector) 3 | 4 | local function useSelector( 5 | hooks, 6 | selector: (state: table) -> any, 7 | equalityFn: ((newState: table, oldState: table) -> boolean)? 8 | ) 9 | return useCustomSelector(hooks, selector, equalityFn, Context) 10 | end 11 | 12 | return useSelector 13 | -------------------------------------------------------------------------------- /src/useStore.lua: -------------------------------------------------------------------------------- 1 | local Context = require(script.Parent.Context) 2 | 3 | local function useStore(hooks) 4 | return hooks.useContext(Context) 5 | end 6 | 7 | return useStore 8 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solarhorizon/rodux-hooks" 3 | description = "Roact hooks for Rodux" 4 | authors = ["SolarHorizon"] 5 | version = "0.3.1" 6 | registry = "https://github.com/UpliftGames/wally-index" 7 | realm = "shared" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | Roact = "roblox/roact@1.4" 12 | --------------------------------------------------------------------------------