Skip to content

Step 6: Event Handlers#

** Quick Tip: Each step has two parts. Part 1 shows you what to build. Part 2** explains why it works. Want to just build? Skip all Part 2 sections!

In this step, you'll learn how to handle user interactions like clicks, typing, and key presses to make your app fully interactive!


Part 1: Building the App#

Step 6.1: Handle Input Changes (onChange)#

Let's make the input field track what you type:

cl import from react {useState}

cl {
    def TodoInput(props: any) -> any {
        return <div style={{
            "display": "flex",
            "gap": "8px",
            "marginBottom": "16px"
        }}>
            <input
                type="text"
                value={props.input}
                onChange={lambda e: any -> None {
                    props.setInput(e.target.value);
                }}
                placeholder="What needs to be done?"
                style={{
                    "flex": "1",
                    "padding": "8px",
                    "border": "1px solid #ddd",
                    "borderRadius": "4px"
                }}
            />
            <button style={{
                "padding": "8px 16px",
                "background": "#3b82f6",
                "color": "white",
                "border": "none",
                "borderRadius": "4px",
                "cursor": "pointer"
            }}>
                Add
            </button>
        </div>;
    }

    def app() -> any {
        let [input, setInput] = useState("");

        return <div style={{
            "maxWidth": "600px",
            "margin": "20px auto",
            "padding": "20px"
        }}>
            <h1>My Todos</h1>
            <TodoInput input={input} setInput={setInput} />
            <p>You typed: "{input}"</p>
        </div>;
    }
}

Try it! Type in the input field - you'll see the text appear below!

Step 6.2: Handle Button Clicks (onClick)#

Now let's make the "Add" button work:

cl import from react {useState}

cl {
    def TodoInput(props: any) -> any {
        return <div style={{
            "display": "flex",
            "gap": "8px",
            "marginBottom": "16px"
        }}>
            <input
                type="text"
                value={props.input}
                onChange={lambda e: any -> None {
                    props.setInput(e.target.value);
                }}
                placeholder="What needs to be done?"
                style={{
                    "flex": "1",
                    "padding": "8px",
                    "border": "1px solid #ddd",
                    "borderRadius": "4px"
                }}
            />
            <button
                onClick={lambda e: any -> None {
                    props.addTodo();
                }}
                style={{
                    "padding": "8px 16px",
                    "background": "#3b82f6",
                    "color": "white",
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                Add
            </button>
        </div>;
    }

    def app() -> any {
        let [todos, setTodos] = useState([]);
        let [input, setInput] = useState("");

        # Function to add a new todo
        def addTodo() -> None {
            if not input.trim() {
                return;  # Don't add empty todos
            }

            let newTodo = {
                "text": input.trim(),
                "done": false
            };

            setTodos(todos.concat([newTodo]));
            setInput("");  # Clear input
        }

        return <div style={{
            "maxWidth": "600px",
            "margin": "20px auto",
            "padding": "20px"
        }}>
            <h1>My Todos</h1>
            <TodoInput
                input={input}
                setInput={setInput}
                addTodo={addTodo}
            />

            # Display todos
            <div>
                {todos.map(lambda todo: any -> any {
                    return <div style={{"padding": "8px"}}>
                        {todo.text}
                    </div>;
                })}
            </div>
        </div>;
    }
}

Try it! Type a todo and click "Add" - it should appear in the list!

Step 6.3: Handle Enter Key (onKeyPress)#

Let's add the ability to press Enter to add a todo:

cl import from react {useState}

cl {
    def TodoInput(props: any) -> any {
        return <div style={{
            "display": "flex",
            "gap": "8px",
            "marginBottom": "16px"
        }}>
            <input
                type="text"
                value={props.input}
                onChange={lambda e: any -> None {
                    props.setInput(e.target.value);
                }}
                onKeyPress={lambda e: any -> None {
                    if e.key == "Enter" {
                        props.addTodo();
                    }
                }}
                placeholder="What needs to be done?"
                style={{
                    "flex": "1",
                    "padding": "8px",
                    "border": "1px solid #ddd",
                    "borderRadius": "4px"
                }}
            />
            <button
                onClick={lambda e: any -> None {
                    props.addTodo();
                }}
                style={{
                    "padding": "8px 16px",
                    "background": "#3b82f6",
                    "color": "white",
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                Add
            </button>
        </div>;
    }

    # ... rest of code
}

Try it! Now you can press Enter to add todos!

Step 6.4: Toggle and Delete Todos#

Let's add the complete functionality:

cl import from react {useState}

cl {
    # ... (keep TodoInput and TodoFilters)

    def TodoItem(props: any) -> any {
        return <div style={{
            "display": "flex",
            "alignItems": "center",
            "gap": "10px",
            "padding": "10px",
            "borderBottom": "1px solid #e5e7eb"
        }}>
            <input
                type="checkbox"
                checked={props.done}
                onChange={lambda e: any -> None {
                    props.toggleTodo(props.id);
                }}
                style={{"cursor": "pointer"}}
            />
            <span style={{
                "flex": "1",
                "textDecoration": ("line-through" if props.done else "none"),
                "color": ("#999" if props.done else "#000")
            }}>
                {props.text}
            </span>
            <button
                onClick={lambda e: any -> None {
                    props.deleteTodo(props.id);
                }}
                style={{
                    "padding": "4px 8px",
                    "background": "#ef4444",
                    "color": "white",
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer",
                    "fontSize": "12px"
                }}
            >
                Delete
            </button>
        </div>;
    }

    def app() -> any {
        let [todos, setTodos] = useState([]);
        let [input, setInput] = useState("");

        # Add todo
        def addTodo() -> None {
            if not input.trim() {
                return;
            }

            let newTodo = {
                "id": Date.now(),  # Use timestamp as unique ID
                "text": input.trim(),
                "done": false
            };

            setTodos(todos.concat([newTodo]));
            setInput("");
        }

        # Toggle todo
        def toggleTodo(id: any) -> None {
            setTodos(todos.map(lambda todo: any -> any {
                if todo["id"] == id {
                    return {
                        "id": todo["id"],
                        "text": todo["text"],
                        "done": not todo["done"]
                    };
                }
                return todo;
            }));
        }

        # Delete todo
        def deleteTodo(id: any) -> None {
            setTodos(todos.filter(lambda todo: any -> bool {
                return todo["id"] != id;
            }));
        }

        return <div style={{
            "maxWidth": "600px",
            "margin": "20px auto",
            "padding": "20px"
        }}>
            <h1>My Todos</h1>
            <TodoInput
                input={input}
                setInput={setInput}
                addTodo={addTodo}
            />

            <div>
                {todos.map(lambda todo: any -> any {
                    return <TodoItem
                        key={todo["id"]}
                        id={todo["id"]}
                        text={todo["text"]}
                        done={todo["done"]}
                        toggleTodo={toggleTodo}
                        deleteTodo={deleteTodo}
                    />;
                })}
            </div>
        </div>;
    }
}

Try it! You can now: - Add todos - Check/uncheck them - Delete them

Step 6.5: Add Filter Functionality#

Final step - make the filter buttons work:

cl import from react {useState}

cl {
    def TodoFilters(props: any) -> any {
        return <div style={{
            "display": "flex",
            "gap": "8px",
            "marginBottom": "16px"
        }}>
            <button
                onClick={lambda e: any -> None {
                    props.setFilter("all");
                }}
                style={{
                    "padding": "6px 12px",
                    "background": ("#3b82f6" if props.filter == "all" else "#e5e7eb"),
                    "color": ("#ffffff" if props.filter == "all" else "#000000"),
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                All
            </button>
            <button
                onClick={lambda e: any -> None {
                    props.setFilter("active");
                }}
                style={{
                    "padding": "6px 12px",
                    "background": ("#3b82f6" if props.filter == "active" else "#e5e7eb"),
                    "color": ("#ffffff" if props.filter == "active" else "#000000"),
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                Active
            </button>
            <button
                onClick={lambda e: any -> None {
                    props.setFilter("completed");
                }}
                style={{
                    "padding": "6px 12px",
                    "background": ("#3b82f6" if props.filter == "completed" else "#e5e7eb"),
                    "color": ("#ffffff" if props.filter == "completed" else "#000000"),
                    "border": "none",
                    "borderRadius": "4px",
                    "cursor": "pointer"
                }}
            >
                Completed
            </button>
        </div>;
    }

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

        # ... (keep addTodo, toggleTodo, deleteTodo functions)

        # 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": "20px auto",
            "padding": "20px"
        }}>
            <h1>My Todos</h1>
            <TodoInput input={input} setInput={setInput} addTodo={addTodo} />
            <TodoFilters filter={filter} setFilter={setFilter} />

            <div>
                {filteredTodos.map(lambda todo: any -> any {
                    return <TodoItem
                        key={todo["id"]}
                        id={todo["id"]}
                        text={todo["text"]}
                        done={todo["done"]}
                        toggleTodo={toggleTodo}
                        deleteTodo={deleteTodo}
                    />;
                })}
            </div>
        </div>;
    }
}

Try it! Now you have a fully functional todo app!


⏭ Want to skip the theory? Jump to Step 7: Effects


Part 2: Understanding the Concepts#

What are Event Handlers?#

Event handlers are functions that run when something happens (user clicks, types, etc.).

Common events: - onClick - User clicks an element - onChange - Input value changes - onKeyPress - User presses a key - onSubmit - Form is submitted - onFocus - Element gains focus - onBlur - Element loses focus

Event Handler Syntax#

<button onClick={lambda e: any -> None {
    # Code runs when button is clicked
    console.log("Clicked!");
}}>
    Click me
</button>

Breakdown: - onClick={} - The event attribute - lambda e: any -> None { } - Anonymous function - e - Event object (contains info about the event)

The Event Object (e)#

onChange={lambda e: any -> None {
    console.log(e.target);        # The element that triggered the event
    console.log(e.target.value);  # For inputs: the current value
    console.log(e.key);           # For key events: which key was pressed
}}

Common properties: - e.target - The element that triggered the event - e.target.value - Current value (for inputs) - e.key - Which key was pressed - e.preventDefault() - Prevent default behavior

Passing Functions as Props#

You can pass functions down to child components:

def Parent() -> any {
    def handleClick() -> None {
        console.log("Clicked!");
    }

    # Pass function to child
    return <Child onClick={handleClick} />;
}

def Child(props: any) -> any {
    # Call parent's function
    return <button onClick={props.onClick}>
        Click me
    </button>;
}

This lets children trigger parent behavior!

Updating State in Event Handlers#

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

    def increment() -> None {
        setCount(count + 1);  # Update state
    }

    return <button onClick={increment}>
        Count: {count}
    </button>;
}

When state updates, React re-renders the component with the new value!

Array Methods for State Updates#

.concat() - Add items

#  Correct way to add
setTodos(todos.concat([newTodo]));

#  Wrong (modifies original)
todos.push(newTodo);
setTodos(todos);

.map() - Update items

# Toggle a todo
setTodos(todos.map(lambda todo: any -> any {
    if todo["id"] == targetId {
        return {"id": todo["id"], "done": not todo["done"]};
    }
    return todo;
}));

.filter() - Remove items

# Delete a todo
setTodos(todos.filter(lambda todo: any -> bool {
    return todo["id"] != targetId;
}));

Inline vs Named Functions#

Inline (good for simple logic):

<button onClick={lambda e: any -> None {
    setCount(count + 1);
}}>
    Click
</button>

Named (good for complex logic):

def handleClick() -> None {
    if count < 10 {
        setCount(count + 1);
    } else {
        alert("Max reached!");
    }
}

<button onClick={handleClick}>Click</button>

Event Handler Common Patterns#

Pattern 1: Toggle Boolean

let [isOpen, setIsOpen] = useState(false);

def toggle() -> None {
    setIsOpen(not isOpen);
}

<button onClick={toggle}>Toggle</button>

Pattern 2: Update Input

let [text, setText] = useState("");

<input
    value={text}
    onChange={lambda e: any -> None {
        setText(e.target.value);
    }}
/>

Pattern 3: Add to List

let [items, setItems] = useState([]);

def addItem(newItem: any) -> None {
    setItems(items.concat([newItem]));
}

Pattern 4: Remove from List

def removeItem(id: any) -> None {
    setItems(items.filter(lambda item: any -> bool {
        return item.id != id;
    }));
}

What You've Learned#

  • What event handlers are
  • Common events (onClick, onChange, onKeyPress)
  • Event handler syntax with lambda functions
  • The event object (e)
  • Passing functions as props
  • Updating state in event handlers
  • Array methods (concat, map, filter)
  • Inline vs named functions

Common Issues#

Issue: Event handler not firing#

Check: - Did you use onClick not onclick? (capital C) - Did you pass a function? onClick={myFunction} not onClick={myFunction()}

Issue: Input not updating#

Check: - Did you add both value and onChange? - Is onChange calling the state setter?

#  Correct
<input
    value={text}
    onChange={lambda e: any -> None {
        setText(e.target.value);
    }}
/>

#  Missing onChange
<input value={text} />

Issue: State not updating#

Check: Are you creating a new array/object?

#  Wrong (modifying original)
todos.push(newTodo);
setTodos(todos);

#  Correct (creating new array)
setTodos(todos.concat([newTodo]));

Quick Exercise#

Try adding a "Clear All" button:

def clearAll() -> None {
    setTodos([]);
}

<button onClick={clearAll}>Clear All</button>

And a "Clear Completed" button:

def clearCompleted() -> None {
    setTodos(todos.filter(lambda todo: any -> bool {
        return not todo["done"];
    }));
}

<button onClick={clearCompleted}>Clear Completed</button>

Next Step#

Excellent! Your app is now fully interactive with local state. But when you refresh the page, all your todos disappear!

In the next step, we'll use useEffect to load data when the app starts!

Continue to Step 7: Effects