Hooks

Hooks are pure functions that allow you to compose a component through function calls. They were created to avoid the class-based nature of class components, and are generally simplier to use than class components too. You must follow the Rules of Hooks at all times while programming with hooks, or otherwise you can encounter undefined behavior that can crash your code (or worse). These two rules are actually pretty simple and are applied the same way in isu, but the second rule is interpreted differently.

In isu, hooks are the building blocks of a component and as such, can only be called in a renderer function passed to isu.component or in the functions that that the renderer calls, as long they remain within the execution flow of the renderer. The latter is often done to compose custom hooks, which you can see in the examples below.

For instance, the following example is invalid:

local x, setX = useState(0) -- used outside of a component, therefore errors
local counter = component(function()
    return 'TextLabel', { Text = x }
end)

The following example is valid:

local counter = component(function()
    local x, setX = useState(0) -- used within a component, therefore works
    return 'TextLabel', { Text = x }
end)

The following example is also valid, since it’s within the execution flow of the rendering function:

local function customHook()
    local x, setX = useState(0)
    -- do something with x/setX
    return x
end

local counter = component(function()
    return 'TextLabel', { Text = customHook() }
end)

Hooks work across yields thanks to contextual coroutines. However, it is easy to make errors by becoming too comfortable with the use of yields and tasks within components. Even though it is technically possible to yield and run asynchronous code within a renderer without issues, it is likely that you will have problems structuring your code in a way that avoids stack overflows or uselessly repeating expensive computations. Sometimes, you can even cause a memory leak by fundamentally misunderstanding the principle of the renderer.

-- DO NOT RUN THIS CODE! This problematic example is provided for educational purposes.
component(function()
    local time, setTime = useState(tick())
    task.spawn(function()
        while task.wait() do
            setTime(tick())
        end
    end)
    return 'TextLabel', { Text = time }
end)

The consequences of running the above code for some time results in the whooping consumption of over 21 GB of memory. Obviously, this is a problem, and it’s caused by inadvertently causing a memory leak by spawning a new task every time the component re-renders.

You wouldn't want your code to crash people's computers, right?

Hooks are provided to fix the above issue. Instead of spawning the task every single time the component renders, you can spawn it once using a useEffect hook, which runs code only when the component is initially mounted.

component(function()
    local time, setTime = useState(tick())
    useEffect(function()
        task.spawn(function()
            while task.wait() do
                setTime(tick())
            end
        end)
    end)
    return 'TextLabel', { Text = time }
end)

Every hook exists to solve a particular problem. If writing something is unexpectedly hard, cumbersome, entirely impossible or strangely erroring for no apparent reason, then it’s most likely that you need to use a hook to accomplish what you want to do. Over time, new hooks may be added to simplify certain recurring problems that prop up during development, but we’ll try to keep it to a minimum.


Table of contents