Skip to content

Lifecycle Hooks in Jac: Component Lifecycle Management#

Learn how to use React's useEffect and useState hooks to manage component state, initialization, side effects, and cleanup.


Table of Contents#


What are Lifecycle Hooks?#

Lifecycle hooks are functions that let you run code at specific points in a component's lifecycle: - When component mounts: Run initialization code once - When component updates: React to state changes - When component unmounts: Clean up resources

Jac uses React hooks as the standard approach: - useState: Manage component state - useEffect: Handle side effects, lifecycle events, and cleanup

Key Benefits: - Initialization: Load data when component appears - Side Effects: Set up subscriptions, timers, or listeners - Reactive Updates: Run code when specific dependencies change - Cleanup: Properly clean up resources when components unmount - Standard React API: Works exactly like React hooks you already know


useState#

The useState hook lets you add state to your components.

Basic Usage#

cl import from react { useState }

cl {
    def Counter() -> any {
        let [count, setCount] = useState(0);

        return <div>
            <h1>Count: {count}</h1>
            <button onClick={lambda e: any -> None {
                setCount(count + 1);
            }}>
                Increment
            </button>
        </div>;
    }
}

Key Points: - Import useState from react - Returns an array: [currentValue, setterFunction] - Use destructuring to get the value and setter - Call the setter function to update state

Multiple State Variables#

cl import from react { useState }

cl {
    def TodoApp() -> any {
        let [todos, setTodos] = useState([]);
        let [inputValue, setInputValue] = useState("");
        let [filter, setFilter] = useState("all");

        return <div>Todo App</div>;
    }
}

useEffect#

The useEffect hook lets you perform side effects in your components. It provides full lifecycle management including mount, update, and cleanup.

Basic Usage - Run on Mount#

cl import from react { useState, useEffect }

cl {
    def MyComponent() -> any {
        let [data, setData] = useState(None);

        useEffect(lambda -> None {
            console.log("Component mounted!");
            # Load initial data
            async def loadData() -> None {
                result = await jacSpawn("get_data", "", {});
                setData(result);
            }
            loadData();
        }, []);  # Empty array means run only on mount

        return <div>My Component</div>;
    }
}

Key Points: - Import useEffect from react - First argument: function to run - Second argument: dependency array - [] - run only on mount - [count] - run when count changes - No array - run on every render

useEffect with Dependencies#

cl import from react { useState, useEffect }

cl {
    def Counter() -> any {
        let [count, setCount] = useState(0);

        useEffect(lambda -> None {
            console.log("Count changed to:", count);
            document.title = "Count: " + str(count);
        }, [count]);  # Run when count changes

        return <div>
            <h1>Count: {count}</h1>
            <button onClick={lambda e: any -> None {
                setCount(count + 1);
            }}>
                Increment
            </button>
        </div>;
    }
}

useEffect with Cleanup#

cl import from react { useEffect }

cl {
    def TimerComponent() -> any {
        useEffect(lambda -> any {
            # Setup
            intervalId = setInterval(lambda -> None {
                console.log("Timer tick");
            }, 1000);

            # Cleanup function (returned from useEffect)
            return lambda -> None {
                clearInterval(intervalId);
            };
        }, []);

        return <div>Timer Component</div>;
    }
}

Common Use Cases#

1. Loading Initial Data#

The most common use case is loading data when a component mounts:

cl import from react { useState, useEffect }
cl import from '@jac-client/utils' { jacSpawn }

cl {
    def TodoApp() -> any {
        let [todos, setTodos] = useState([]);
        let [loading, setLoading] = useState(True);

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                setLoading(True);

                # Fetch todos from backend
                result = await jacSpawn("read_todos", "", {});
                console.log(result);
                setTodos(result.reports);
                setLoading(False);
            }
            loadTodos();
        }, []);  # Empty array = run only on mount

        if loading {
            return <div>Loading...</div>;
        }

        return <div>
            {todos.map(lambda todo: any -> any {
                return <TodoItem todo={todo} />;
            })}
        </div>;
    }
}

2. Setting Up Event Listeners#

Set up event listeners with proper cleanup:

cl import from react { useState, useEffect }

cl {
    def WindowResizeHandler() -> any {
        let [width, setWidth] = useState(0);
        let [height, setHeight] = useState(0);

        useEffect(lambda -> any {
            def handleResize() -> None {
                setWidth(window.innerWidth);
                setHeight(window.innerHeight);
            }

            # Set initial size
            handleResize();

            # Add listener
            window.addEventListener("resize", handleResize);

            # Cleanup function
            return lambda -> None {
                window.removeEventListener("resize", handleResize);
            };
        }, []);

        return <div>
            Window size: {width} x {height}
        </div>;
    }
}

3. Fetching User Data#

Load user-specific data when a component mounts:

cl import from react { useState, useEffect }
cl import from '@jac-client/utils' { jacSpawn }

cl {
    def ProfileView() -> any {
        let [profile, setProfile] = useState(None);
        let [loading, setLoading] = useState(True);

        useEffect(lambda -> None {
            async def loadUserProfile() -> None {
                if not jacIsLoggedIn() {
                    navigate("/login");
                    return;
                }

                # Fetch user profile
                result = await jacSpawn("get_user_profile", "", {});
                setProfile(result);
                setLoading(False);
            }
            loadUserProfile();
        }, []);

        if loading {
            return <div>Loading profile...</div>;
        }

        if not profile {
            return <div>No profile found</div>;
        }

        return <div>
            <h1>{profile.username}</h1>
            <p>{profile.email}</p>
        </div>;
    }
}

4. Initializing Third-Party Libraries#

Initialize external libraries or APIs:

cl import from react { useEffect }

cl {
    def ChartComponent() -> any {
        useEffect(lambda -> any {
            # Initialize chart library
            chart = new Chart("myChart", {
                "type": "line",
                "data": chartData,
                "options": chartOptions
            });

            # Cleanup function
            return lambda -> None {
                chart.destroy();
            };
        }, []);

        return <canvas id="myChart"></canvas>;
    }
}

5. Focusing Input Fields#

Focus an input field when a component mounts:

cl import from react { useEffect }

cl {
    def SearchBar() -> any {
        useEffect(lambda -> None {
            # Focus search input on mount
            inputEl = document.getElementById("search-input");
            if inputEl {
                inputEl.focus();
            }
        }, []);

        return <input
            id="search-input"
            type="text"
            placeholder="Search..."
        />;
    }
}

Complete Examples#

Example 1: Todo App with Data Loading#

cl import from react { useState, useEffect }
cl import from '@jac-client/utils' { jacSpawn }

cl {
    def app() -> any {
        let [todos, setTodos] = useState([]);
        let [inputValue, setInputValue] = useState("");
        let [filter, setFilter] = useState("all");

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                todos = await jacSpawn("read_todos","",{});
                console.log(todos);
                setTodos(todos.reports);
            }
            loadTodos();
        }, []);

        # Add a new todo
        async def addTodo() -> None {
            if not inputValue.trim() { return; }
            newTodo = {
                "id": Date.now(),
                "text": inputValue.trim(),
                "done": False
            };
            await jacSpawn("create_todo","", {"text": inputValue.trim()});
            newTodos = todos.concat([newTodo]);
            setTodos(newTodos);
            setInputValue("");
        }

        # Toggle todo completion status
        async def toggleTodo(id: any) -> None {
            await jacSpawn("toggle_todo",id, {});
            setTodos(todos.map(lambda todo: any -> any {
                if todo._jac_id == id {
                    updatedTodo = {
                        "_jac_id": todo._jac_id,
                        "text": todo.text,
                        "done": not todo.done,
                        "id": todo.id
                    };
                    return updatedTodo;
                }
                return todo;
            }));
        }

        # Filter todos based on current filter
        def getFilteredTodos() -> list {
            if filter == "active" {
                return todos.filter(lambda todo: any -> bool { return not todo.done; });
            } elif filter == "completed" {
                return todos.filter(lambda todo: any -> bool { return todo.done; });
            }
            return todos;
        }

        filteredTodos = getFilteredTodos();

        return <div style={{
            "maxWidth": "600px",
            "margin": "40px auto",
            "padding": "24px",
            "fontFamily": "system-ui, -apple-system, sans-serif"
        }}>
            <h1 style={{"textAlign": "center"}}> My Todo App</h1>

            # Add todo form
            <div style={{"display": "flex", "gap": "8px", "marginBottom": "24px"}}>
                <input
                    type="text"
                    value={inputValue}
                    onChange={lambda e: any -> None { setInputValue(e.target.value); }}
                    onKeyPress={lambda e: any -> None {
                        if e.key == "Enter" { addTodo(); }
                    }}
                    placeholder="What needs to be done?"
                    style={{"flex": "1", "padding": "12px"}}
                />
                <button onClick={addTodo} style={{"padding": "12px 24px"}}>
                    Add
                </button>
            </div>

            # Filter buttons
            <div style={{"display": "flex", "gap": "8px", "marginBottom": "16px"}}>
                <button onClick={lambda -> None { setFilter("all"); }}>All</button>
                <button onClick={lambda -> None { setFilter("active"); }}>Active</button>
                <button onClick={lambda -> None { setFilter("completed"); }}>Completed</button>
            </div>

            # Todo list
            <ul>
                {filteredTodos.map(lambda todo: any -> any {
                    return <li key={todo._jac_id}>
                        <input
                            type="checkbox"
                            checked={todo.done}
                            onChange={lambda -> None { toggleTodo(todo._jac_id); }}
                        />
                        <span>{todo.text}</span>
                    </li>;
                })}
            </ul>
        </div>;
    }
}

Example 2: Dashboard with Multiple Data Sources#

cl import from react { useState, useEffect }
cl import from '@jac-client/utils' { jacSpawn }

cl {
    def Dashboard() -> any {
        let [stats, setStats] = useState(None);
        let [activity, setActivity] = useState([]);
        let [loading, setLoading] = useState(True);

        useEffect(lambda -> None {
            async def loadDashboardData() -> None {
                setLoading(True);

                # Load multiple data sources in parallel
                results = await Promise.all([
                    jacSpawn("get_stats", "", {}),
                    jacSpawn("get_recent_activity", "", {})
                ]);

                setStats(results[0]);
                setActivity(results[1].reports);
                setLoading(False);
            }
            loadDashboardData();
        }, []);

        if loading {
            return <div>Loading dashboard...</div>;
        }

        return <div>
            <StatsView stats={stats} />
            <ActivityList activities={activity} />
        </div>;
    }
}

Example 3: Timer Component with Cleanup#

Proper cleanup when component unmounts:

cl import from react { useState, useEffect }

cl {
    def TimerComponent() -> any {
        let [seconds, setSeconds] = useState(0);

        useEffect(lambda -> any {
            # Set up timer
            intervalId = setInterval(lambda -> None {
                setSeconds(lambda prev: int -> int { return prev + 1; });
            }, 1000);

            # Cleanup function - runs when component unmounts
            return lambda -> None {
                clearInterval(intervalId);
            };
        }, []);

        return <div>Timer: {seconds} seconds</div>;
    }
}

Best Practices#

1. Always Specify Dependencies#

Be explicit about what your effect depends on:

#  Good: Empty array for mount-only effects
useEffect(lambda -> None {
    loadInitialData();
}, []);

#  Good: Specify dependencies
useEffect(lambda -> None {
    console.log("Count changed:", count);
}, [count]);

#  Warning: No dependency array runs on every render
useEffect(lambda -> None {
    console.log("Runs on every render!");
});

2. Handle Async Operations Properly#

Always handle async operations with proper error handling:

#  Good: Proper async handling
useEffect(lambda -> None {
    async def loadData() -> None {
        try {
            data = await jacSpawn("get_data", "", {});
            setData(data);
        } except Exception as err {
            console.error("Error loading data:", err);
            setError(err);
        }
    }
    loadData();
}, []);

3. Clean Up Side Effects#

Always clean up event listeners, timers, and subscriptions:

#  Good: Cleanup function removes event listener
useEffect(lambda -> any {
    def handleResize() -> None {
        setWidth(window.innerWidth);
    }

    window.addEventListener("resize", handleResize);

    return lambda -> None {
        window.removeEventListener("resize", handleResize);
    };
}, []);

4. Use Loading States#

Show loading indicators while data is being fetched:

#  Good: Clear loading states
def Component() -> any {
    let [data, setData] = useState(None);
    let [loading, setLoading] = useState(True);
    let [error, setError] = useState(None);

    useEffect(lambda -> None {
        async def loadData() -> None {
            try {
                setLoading(True);
                result = await jacSpawn("get_data", "", {});
                setData(result);
            } except Exception as err {
                setError(err);
            } finally {
                setLoading(False);
            }
        }
        loadData();
    }, []);

    if loading { return <div>Loading...</div>; }
    if error { return <div>Error: {error}</div>; }
    return <div>{data}</div>;
}

5. Keep Effects Focused#

Each effect should have a single responsibility:

#  Good: Separate effects for separate concerns
def Component() -> any {
    useEffect(lambda -> None {
        loadData();  # Data loading
    }, []);

    useEffect(lambda -> any {
        # Event listener setup
        window.addEventListener("resize", handleResize);
        return lambda -> None {
            window.removeEventListener("resize", handleResize);
        };
    }, []);

    return <div>Component</div>;
}

6. Avoid Stale Closures#

Be careful with closures capturing old state values:

#  Avoid: Stale closure problem
useEffect(lambda -> None {
    setInterval(lambda -> None {
        setCount(count + 1);  # count is stale!
    }, 1000);
}, []);

#  Good: Use functional update
useEffect(lambda -> any {
    intervalId = setInterval(lambda -> None {
        setCount(lambda prev: int -> int { return prev + 1; });
    }, 1000);

    return lambda -> None {
        clearInterval(intervalId);
    };
}, []);

Summary#

  • useState: Manage component state (replaces createState(), createSignal())
  • useEffect: Handle side effects and lifecycle events (replaces onMount(), createEffect())
  • Dependencies: Always specify what your effect depends on
  • Cleanup: Return a cleanup function for subscriptions, timers, and listeners
  • Best Practices: Handle errors, use loading states, keep effects focused

React hooks provide a powerful and standard way to manage component lifecycle!


Legacy Jac Hooks#

Note: The following hooks are from older Jac versions. New projects should use React hooks instead.

onMount() - Legacy#

The onMount() hook was a Jac-specific hook for running code once when a component mounts:

# Legacy approach - use useEffect instead
def Component() -> any {
    onMount(lambda -> None {
        loadData();
    });
    return <div>Component</div>;
}

Modern equivalent:

# Modern approach with React hooks
def Component() -> any {
    useEffect(lambda -> None {
        loadData();
    }, []);
    return <div>Component</div>;
}

createState() - Legacy#

The createState() hook was a Jac-specific state management solution:

# Legacy approach - use useState instead
let [state, setState] = createState({"count": 0});

def Component() -> any {
    s = state();
    return <div>{s.count}</div>;
}

Modern equivalent:

# Modern approach with React hooks
def Component() -> any {
    let [count, setCount] = useState(0);
    return <div>{count}</div>;
}

createSignal() and createEffect() - Legacy#

These were Signal-based reactive primitives from Jac:

# Legacy approach
let [count, setCount] = createSignal(0);

createEffect(lambda -> None {
    console.log("Count:", count());
});

Modern equivalent:

# Modern approach with React hooks
let [count, setCount] = useState(0);

useEffect(lambda -> None {
    console.log("Count:", count);
}, [count]);