Async
In reactive applications, managing asynchronous operations like DataStore requests, fetching player thumbnails, or HTTP calls can be complicated. You have to track loading states, handle potential errors, and ensure that if a user changes their input quickly, the application doesn't render an outdated, delayed response.
Flux.async solves this by wrapping yielding operations in a secure, reactive container. If you are familiar with SolidJS, this is the equivalent of createResource.
Creating an Async Node
To create an asynchronous reactive node, you use Flux.async (or Scope:async if you are managing lifecycles via a Scope). Because of how Flux tracks dependencies, you only need to provide a single function, but you must follow a strict rule regarding when you read reactive state.
It requires two arguments:
- The Asynchronous Function: The function that performs the yielding work (e.g., calling
:GetAsync()ortask.wait). - Initial Value (Optional): A starting value to hold in the state before the very first fetch completes.
⚠️ The "Read Before Yield" Rule
Flux tracks dependencies synchronously. When your Async function runs, it executes immediately until it hits its first yield. You must extract all reactive dependencies at the top of your function, before you yield. Any reactive nodes read after a yield will not be tracked, and your Async block will not re-run when they change.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Flux = require(ReplicatedStorage.Flux)
-- A reactive Value holding the target User ID
local currentUserId = Flux(1)
-- Create the Async resource
local profileData = Flux.async(
function()
-- 1. READ BEFORE YIELD: Extract reactive state synchronously
local userId = currentUserId()
-- 2. YIELD: Perform the async operation
-- Imagine yielding to a DataStore or external API here
task.wait(1)
-- 3. RETURN: Pass back the resolved data
return { name = "Player_" .. userId, level = userId * 10 }
end,
-- Initial Value
nil
)Reading Async State
The object returned by Flux.async contains three reactive properties, which are all standard Flux Nodes. You can bind these directly to your UI to conditionally render loading spinners, error messages, or the final data.
data: Holds the result of the async function. (Defaults to theinitialValueorniluntil resolved).loading: A boolean Value that istruewhile the fetcher is yielding.error: A string Value that populates if the fetcher function throws an error (viapcall). Otherwise, it isnil.
local new = Flux.new
local ui = new "TextLabel" {
Name = "ProfileCard",
Size = UDim2.fromOffset(300, 100),
-- Bind a function to conditionally render based on the Async state
Text = function()
if profileData.loading() then
return "Loading profile..."
end
if profileData.error() then
return "Failed to load: " .. profileData.error()
end
local data = profileData.data()
if data then
return `Name: {data.name} | Level: {data.level}`
end
return "No data available."
end
}Automatic Race Condition Handling
One of the most powerful features of Flux.async is how it handles race conditions.
If your dependencies change rapidly (for example, if a player rapidly clicks through a list of User IDs), the Async node will spawn multiple fetch requests. However, Flux internally tracks the ID of every request.
If an older request finally resolves after a newer request has already started, the outdated data is completely ignored. Your .data and .error nodes will only ever update with the result of the most recent request, ensuring your UI state is never polluted by slow, out-of-date network responses.
Cleaning Up
If an Async node is no longer needed, you can completely destroy it to free up memory and destroy its internal data, loading, and error nodes.
profileData:Destroy()Note: If you create the resource using Scope:async(), it will be automatically destroyed when the Scope is destroyed, keeping your memory management completely hands-off.