Rodux

repository·master·Indexed 43 days ago

https://github.com/roblox/rodux

A state management library for Roblox Lua heavily inspired by the Redux pattern. It provides a Store implementation with support for reducers, middleware (including built-in thunk and logger middlewares), custom Devtools for state introspection, and ErrorReporters for intercepting reducer and update errors.

Tokens
12.5K
Snippets
41
Records
55
Agent score
52%

What's inside Rodux

  1. What is a reducer in Rodux?

    master

    A reducer is a function provided to Store.new that determines how the application state changes in response to dispatched actions.

    Key principles of reducers:

    1. Input: It receives the current state and the dispatched action.
    2. Output: It must return a new state object.
    3. Immutability: The state is read-only. You must never modify the existing state object directly; instead, you must construct and return a new object (e.g., a new table) representing the updated state.
    4. Default Behavior: If the reducer does not handle a specific action type, it should return the current state unchanged.
    local phoneNumberReducer = function(state, action)
    	if action.type == "ReceivedNewPhoneNumber" then
    		return action.phoneNumber
    	end
    
    	return state
    end
  2. What is middleware and when to use it

    master

    In Rodux, Store:dispatch typically sends actions directly to the reducer. However, if you need to perform side effects that the reducer cannot handle, you should use middleware.

    Common use cases for middleware include:

    • Delayed processing: Delaying an action before it reaches the reducer.
    • Logging: Monitoring actions dispatched to the store.
    • Asynchronous side effects: Performing network requests in response to an action and then dispatching a new action with the result to update the state.
  3. What is a Rodux Store?

    master

    The store is the main entry point for all Rodux operations. It is composed of three core parts: State, Reducer, and Dispatcher. You initialize a store using Store.new.

    local store = Rodux.Store.new(function(action, currentState)
    	-- The body of your reducer
    end)
  4. Understand the Dispatcher in Rodux

    master

    The Dispatcher is the function within the store responsible for handling incoming action objects and forwarding them to the reducer.

    If you provide middleware during initialization via Store.new, the middleware will wrap the dispatcher. This ensures that every action passes through all provided middleware before reaching the reducer.

  5. Core principles of Rodux state management

    master

    Rodux is designed to manage complex application state by adhering to three fundamental principles:

    1. Single Source of Truth: All application state is collected into a single object. This allows for easy inspection of the entire data set backing your business logic and prevents data from being scattered across disparate models.
    2. State is Read-only: The state object cannot be modified directly. All changes must be accomplished by dispatching actions to the store. This makes state changes transparent, reproducible, and enables features like undo/redo.
    3. Changes Are Made With Pure Functions: Actions are processed by reducers. A reducer is a pure function that takes the current state and an action as input and returns a new state object. Reducers have no side effects, ensuring business logic is centralized and predictable.
  6. Understand Reducers in Rodux

    master

    A Reducer is a pure function provided to Store.new during initialization. It accepts two arguments:

    1. action: An object describing the change that occurred.
    2. currentState: The current state of the store.

    The reducer must return a new state object in response to the action. For complex applications, reducers are typically composed of multiple smaller pure functions that each manage a specific slice of the state.

  7. How to compose complex state with sub-reducers

    master

    In large applications, a single global reducer becomes difficult to manage. Instead, you can break the state down into smaller portions, where each sub-reducer is responsible for a specific slice of the state.

    A root reducer then calls these sub-reducers, passing only the relevant slice of the state to each one.

    local friendsReducer = function(state, action)
    	state = state or {}
    
    	if action.type == "MadeNewFriends" then
    		local newState = {}
    
    		-- Copy existing state to maintain immutability
    		for index, friend in ipairs(state) do
    			newState[index] = friend
    		end
    
    		for _, friend in ipairs(action.newFriends) do
    			table.insert(newState, friend)
    		end
    
    		return newState
    	end
    
    	return state
    end
    
    -- The root reducer delegates slices of state to sub-reducers
    local reducer = function(state, action)
    	return {
    		myPhoneNumber = phoneNumberReducer(state.myPhoneNumber, action),
    		myFriends = friendsReducer(state.myFriends, action),
    	}
    end
  8. How to use _hookIntoStore to intercept Store operations

    master

    The :_hookIntoStore(store) method is the primary extension point for devtools. When Rodux calls this method, it passes the store instance to your object.

    To monitor or modify store behavior, you can wrap the store's internal properties. Common targets for wrapping include:

    • store._reducer: Wrap this to profile the time taken to process actions or to log state changes.
    • store._flushHandler: Wrap this to monitor when the store notifies listeners of changes.

    Note: When wrapping these functions, ensure you capture the original function (e.g., self._originalReducer = store._reducer) so you can call it within your wrapper to maintain standard Rodux behavior.

    function Devtools:_hookIntoStore(store)
        -- Capture original reducer
        local originalReducer = store._reducer
    
        -- Replace with a wrapped version
        store._reducer = function(state, action)
            local result = originalReducer(state, action)
            -- Perform custom logic (e.g., logging, profiling)
            print("Action processed:", action.type)
            return result
        end
    end
  9. What are thunks and how do they work?

    master

    Thunks are a way to introduce complex processing or asynchronous logic into your application. When using thunkMiddleware, any action dispatched to the store that is a Lua function (instead of a table) is intercepted. The middleware executes this function instead of forwarding it to the reducer.

    Thunks receive the store as an argument, allowing them to perform side effects (like API calls) and dispatch new action objects (tables) to update the state once the side effect is complete.

    -- A thunk is a function dispatched to the store
    store:dispatch(function(store)
        -- Perform side effect
        getAsyncData(function(result)
            -- Dispatch a standard action table to update state
            store:dispatch({
                type = "DATA_LOADED",
                payload = result,
            })
        end)
    end)
  10. Implement custom middleware

    master

    Middleware allows you to intercept actions before they reach the reducer. A middleware is a function with the following signature:

    (nextDispatch, store) -> (action) -> result

    • nextDispatch: The next dispatch function in the chain.
    • store: The current store instance.
    • The returned function is called whenever an action is dispatched.

    Important: Middleware runs from left to right. If a middleware does not call nextDispatch, subsequent middlewares in the chain will not run.

    local function simpleLogger(nextDispatch, store)
    	return function(action)
    		print("Dispatched action of type", action.type)
    		return nextDispatch(action)
    	end
    end
    
    local store = Store.new(reducer, initialState, { simpleLogger })
  11. Understand State in Rodux

    master

    The State is a Lua table that contains all the data backing your application's business logic. It is immutable in practice (the reducer returns a new state rather than mutating the old one) and can be read at any time using the Store:getState method.

    local exampleState = {
    	myPhoneNumber = "15552345678",
    	myFriends = {
    		"Joe",
    		"Cassandra",
    	},
    }
  12. How to implement a custom middleware

    master

    A middleware is a higher-order function with the following signature: function(nextDispatch, store) -> function(action)

    1. It accepts nextDispatch (the function to pass the action to the next middleware or the reducer) and the store instance.
    2. It returns a new function that receives the action.
    3. Crucial: You must call nextDispatch(action) within your middleware; otherwise, the action will be swallowed and will never reach the reducer or subsequent middlewares.

    Middlewares are executed in the order they are provided to Store.new (from left to right).

    local reducer = function(state, action)
    	-- the body of your reducer
    end
    
    local initialState = {}
    
    local delayOneSecondMiddleware = function(nextDispatch, store)
    	return function(action)
    		delay(1, function()
    			-- nextDispatch passes the action to the next middleware or the reducer
    			nextDispatch(action)
    		end)
    	end
    end
    
    local store = Rodux.Store.new(reducer, initialState, {
    	delayOneSecondMiddleware,
    })