Skip to content

Advanced State Management in Jac#

Learn how to manage complex state in Jac using React hooks, combining multiple state instances, and building scalable state architectures.


Table of Contents#


React Hooks Overview#

Jac uses React hooks for all state management. The most common hooks are:

  • useState: Manage component state
  • useEffect: Handle side effects and lifecycle
  • useReducer: Manage complex state logic
  • useContext: Share state across components
  • useMemo: Memoize expensive computations
  • useCallback: Memoize callback functions

Basic useState Example#

cl import from react { useState, useEffect }

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

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                setLoading(True);
                result = root spawn read_todos();
                setTodos(result.reports);
                setLoading(False);
            }
            loadTodos();
        }, []);

        return <div>{/* your UI */}</div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Multiple State Variables#

Separating State by Concern#

Instead of putting everything in one state object, split state into multiple variables:

cl import from react { useState, useEffect }

cl {
    def TodoApp() -> any {
        # Separate state variables for different concerns
        let [todos, setTodos] = useState([]);
        let [filter, setFilter] = useState("all");
        let [loading, setLoading] = useState(False);
        let [error, setError] = useState(None);

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                setLoading(True);
                setError(None);
                try {
                    result = root spawn read_todos();
                    setTodos(result.reports);
                } catch (err) {
                    setError(err);
                } finally {
                    setLoading(False);
                }
            }
            loadTodos();
        }, []);

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

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

    def app() -> any {
        return <TodoApp />;
    }
}

Benefits: - Separation of Concerns: Each state variable manages one aspect - Selective Updates: Only components using specific state re-render - Type Safety: Each variable has its own type - Clearer Code: Easy to understand what each state represents

When to Use Object State#

Sometimes an object makes sense for closely related data:

cl import from react { useState }

cl {
    def UserProfile() -> any {
        # Good: Related data in one object
        let [user, setUser] = useState({
            "name": "",
            "email": "",
            "avatar": ""
        });

        def updateName(name: str) -> None {
            setUser({...user, "name": name});
        }

        return <div>
            <input value={user.name} onChange={lambda e: any -> None {
                updateName(e.target.value);
            }} />
        </div>;
    }
}

State Composition Patterns#

Pattern 1: Feature-Based State#

Organize state by feature or domain using multiple useState calls:

cl import from react { useState, useEffect }

cl {
    def App() -> any {
        # User state
        let [user, setUser] = useState(None);
        let [isLoggedIn, setIsLoggedIn] = useState(False);

        # Todo state
        let [todos, setTodos] = useState([]);
        let [selectedId, setSelectedId] = useState(None);

        # UI state
        let [theme, setTheme] = useState("light");
        let [sidebarOpen, setSidebarOpen] = useState(False);
        let [modalOpen, setModalOpen] = useState(False);

        # Settings state
        let [notifications, setNotifications] = useState(True);
        let [language, setLanguage] = useState("en");

        useEffect(lambda -> None {
            async def loadData() -> None {
                result = root spawn get_user_data();
                setUser(result.user);
                setIsLoggedIn(True);
            }
            loadData();
        }, []);

        return <div className={theme}>
            {sidebarOpen and <Sidebar />}
            {todos.length > 0 and <TodoList items={todos} />}
        </div>;
    }

    def app() -> any {
        return <App />;
    }
}

Pattern 2: Local vs Global State#

Use Context for global state and useState for local state:

cl import from react { useState, useContext, createContext }

cl {
    # Create context for global state
    AppContext = createContext(None);

    def App() -> any {
        # Global state
        let [currentUser, setCurrentUser] = useState(None);
        let [theme, setTheme] = useState("light");

        appValue = {
            "currentUser": currentUser,
            "theme": theme,
            "setCurrentUser": setCurrentUser,
            "setTheme": setTheme
        };

        return <AppContext.Provider value={appValue}>
            <TodoForm />
            <TodoList />
        </AppContext.Provider>;
    }

    # Component with local state
    def TodoForm() -> any {
        # Access global context
        app = useContext(AppContext);

        # Local component state
        let [text, setText] = useState("");
        let [valid, setValid] = useState(False);

        def validate(value: str) -> None {
            setValid(len(value.trim()) > 0);
        }

        return <form>
            <input
                value={text}
                onChange={lambda e: any -> None {
                    newText = e.target.value;
                    setText(newText);
                    validate(newText);
                }}
                style={{"background": ("#333" if app.theme == "dark" else "#fff")}}
            />
        </form>;
    }

    def TodoList() -> any {
        # Local list state
        let [sortBy, setSortBy] = useState("date");
        let [order, setOrder] = useState("asc");

        # Access global context
        app = useContext(AppContext);

        return <div>
            <h2>Welcome, {app.currentUser.name if app.currentUser else "Guest"}</h2>
        </div>;
    }

    def app() -> any {
        return <App />;
    }
}

Pattern 3: Custom Hooks (State Modules)#

Create reusable custom hooks for shared logic:

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

cl {
    # Custom hook: User management
    def useUser() -> dict {
        let [user, setUser] = useState(None);
        let [loading, setLoading] = useState(False);
        let [error, setError] = useState(None);

        async def loadUser() -> None {
            setLoading(True);
            setError(None);
            try {
                result = root spawn get_current_user();
                setUser(result);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(False);
            }
        }

        def logout() -> None {
            jacLogout();
            setUser(None);
        }

        useEffect(lambda -> None {
            loadUser();
        }, []);

        return {
            "user": user,
            "loading": loading,
            "error": error,
            "logout": logout,
            "reload": loadUser
        };
    }

    # Custom hook: Todo management
    def useTodos() -> dict {
        let [todos, setTodos] = useState([]);
        let [loading, setLoading] = useState(False);

        async def loadTodos() -> None {
            setLoading(True);
            try {
                result = root spawn read_todos();
                setTodos(result.reports);
            } finally {
                setLoading(False);
            }
        }

        async def addTodo(text: str) -> None {
            response = root spawn create_todo(text=text);
            new_todo = response.reports[0][0];
            setTodos(todos.concat([new_todo]));
        }

        async def toggleTodo(id: str) -> None {
            id spawn toggle_todo();
            setTodos(todos.map(lambda todo: any -> any {
                if todo._jac_id == id {
                    return {...todo, "done": not todo.done};
                }
                return todo;
            }));
        }

        useEffect(lambda -> None {
            loadTodos();
        }, []);

        return {
            "todos": todos,
            "loading": loading,
            "addTodo": addTodo,
            "toggleTodo": toggleTodo,
            "reload": loadTodos
        };
    }

    # Using custom hooks in components
    def TodoApp() -> any {
        userData = useUser();
        todoData = useTodos();

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

        return <div>
            <h1>Welcome, {userData.user.name if userData.user else "Guest"}</h1>
            <button onClick={userData.logout}>Logout</button>
            {todoData.todos.map(lambda todo: any -> any {
                return <TodoItem key={todo._jac_id} todo={todo} />;
            })}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Derived State#

Computed Values with useMemo#

Use useMemo to memoize expensive computations:

cl import from react { useState, useMemo }

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

        # Memoized filtered todos - only recomputes when todos or filter changes
        filteredTodos = useMemo(lambda -> list {
            if filter == "active" {
                return todos.filter(lambda item: any -> bool { return not item.done; });
            } elif filter == "completed" {
                return todos.filter(lambda item: any -> bool { return item.done; });
            }
            return todos;
        }, [todos, filter]);

        # Memoized stats - only recomputes when todos changes
        stats = useMemo(lambda -> dict {
            total = todos.length;
            active = todos.filter(lambda item: any -> bool { return not item.done; }).length;
            completed = total - active;

            return {
                "total": total,
                "active": active,
                "completed": completed
            };
        }, [todos]);

        return <div>
            <div>
                Total: {stats.total}, Active: {stats.active}, Completed: {stats.completed}
            </div>
            {filteredTodos.map(lambda item: any -> any {
                return <TodoItem key={item._jac_id} todo={item} />;
            })}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Simple Derived Values#

For simple computations, you don't need useMemo:

cl import from react { useState }

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

        # Simple derived values - computed on each render
        def getFilteredTodos() -> list {
            if filter == "active" {
                return todos.filter(lambda item: any -> bool { return not item.done; });
            } elif filter == "completed" {
                return todos.filter(lambda item: any -> bool { return item.done; });
            }
            return todos;
        }

        filtered = getFilteredTodos();
        activeCount = todos.filter(lambda item: any -> bool { return not item.done; }).length;

        return <div>
            <div>{activeCount} active todos</div>
            {filtered.map(lambda item: any -> any {
                return <TodoItem key={item._jac_id} todo={item} />;
            })}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Reactive Updates with useEffect#

Use useEffect to sync derived state or perform side effects:

cl import from react { useState, useEffect }

cl {
    def TodoApp() -> any {
        let [todos, setTodos] = useState([]);
        let [stats, setStats] = useState({
            "total": 0,
            "active": 0,
            "completed": 0
        });

        # Update stats whenever todos change
        useEffect(lambda -> None {
            total = todos.length;
            active = todos.filter(lambda item: any -> bool { return not item.done; }).length;
            completed = total - active;

            setStats({
                "total": total,
                "active": active,
                "completed": completed
            });

            # Optional: Save to localStorage
            localStorage.setItem("todoStats", JSON.stringify(stats));
        }, [todos]);

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

    def app() -> any {
        return <TodoApp />;
    }
}

Advanced React Hooks#

useReducer for Complex State#

When state logic becomes complex, use useReducer instead of useState:

cl import from react { useReducer, useEffect }

cl {
    # Reducer function
    def todoReducer(state: dict, action: dict) -> dict {
        type = action.type;

        if type == "ADD_TODO" {
            return {...state, "todos": state.todos.concat([action.payload])};
        } elif type == "TOGGLE_TODO" {
            return {
                ...state,
                "todos": state.todos.map(lambda todo: any -> any {
                    if todo._jac_id == action.payload {
                        return {...todo, "done": not todo.done};
                    }
                    return todo;
                })
            };
        } elif type == "REMOVE_TODO" {
            return {
                ...state,
                "todos": state.todos.filter(lambda todo: any -> bool {
                    return todo._jac_id != action.payload;
                })
            };
        } elif type == "SET_FILTER" {
            return {...state, "filter": action.payload};
        } elif type == "SET_LOADING" {
            return {...state, "loading": action.payload};
        }

        return state;
    }

    def TodoApp() -> any {
        # Initial state
        initialState = {
            "todos": [],
            "filter": "all",
            "loading": False
        };

        let [state, dispatch] = useReducer(todoReducer, initialState);

        useEffect(lambda -> None {
            async def loadTodos() -> None {
                dispatch({"type": "SET_LOADING", "payload": True});
                result = root spawn read_todos();
                for todo in result.reports {
                    dispatch({"type": "ADD_TODO", "payload": todo});
                }
                dispatch({"type": "SET_LOADING", "payload": False});
            }
            loadTodos();
        }, []);

        async def addTodo(text: str) -> None {
            response = root spawn create_todo(text=text);
            new_todo = response.reports[0][0];
            dispatch({"type": "ADD_TODO", "payload": new_todo});
        }

        def toggleTodo(id: str) -> None {
            dispatch({"type": "TOGGLE_TODO", "payload": id});
        }

        return <div>
            {state.loading and <div>Loading...</div>}
            {state.todos.map(lambda todo: any -> any {
                return <TodoItem
                    key={todo._jac_id}
                    todo={todo}
                    onToggle={lambda -> None { toggleTodo(todo._jac_id); }}
                />;
            })}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

useContext for Global State#

Share state across multiple components without prop drilling:

cl import from react { useState, useContext, createContext }

cl {
    # Create context
    TodoContext = createContext(None);

    # Provider component
    def TodoProvider(props: dict) -> any {
        let [todos, setTodos] = useState([]);
        let [filter, setFilter] = useState("all");

        async def addTodo(text: str) -> None {
            response = root spawn create_todo(text=text);
            new_todo = response.reports[0][0];
            setTodos(todos.concat([new_todo]));
        }

        async def toggleTodo(id: str) -> None {
            id spawn toggle_todo();
            setTodos(todos.map(lambda todo: any -> any {
                if todo._jac_id == id {
                    return {...todo, "done": not todo.done};
                }
                return todo;
            }));
        }

        value = {
            "todos": todos,
            "filter": filter,
            "setFilter": setFilter,
            "addTodo": addTodo,
            "toggleTodo": toggleTodo
        };

        return <TodoContext.Provider value={value}>
            {props.children}
        </TodoContext.Provider>;
    }

    # Hook to use context
    def useTodoContext() -> dict {
        return useContext(TodoContext);
    }

    # Components using the context
    def TodoList() -> any {
        ctx = useTodoContext();

        filteredTodos = ctx.todos.filter(lambda todo: any -> bool {
            if ctx.filter == "active" { return not todo.done; }
            if ctx.filter == "completed" { return todo.done; }
            return True;
        });

        return <div>
            {filteredTodos.map(lambda todo: any -> any {
                return <TodoItem
                    key={todo._jac_id}
                    todo={todo}
                    onToggle={lambda -> None { ctx.toggleTodo(todo._jac_id); }}
                />;
            })}
        </div>;
    }

    def FilterButtons() -> any {
        ctx = useTodoContext();

        return <div>
            <button onClick={lambda -> None { ctx.setFilter("all"); }}>All</button>
            <button onClick={lambda -> None { ctx.setFilter("active"); }}>Active</button>
            <button onClick={lambda -> None { ctx.setFilter("completed"); }}>Completed</button>
        </div>;
    }

    # App with provider
    def MainApp() -> any {
        return <TodoProvider>
            <FilterButtons />
            <TodoList />
        </TodoProvider>;
    }

    def app() -> any {
        return <MainApp />;
    }
}

useCallback for Memoized Functions#

Prevent unnecessary re-renders by memoizing callbacks:

cl import from react { useState, useCallback }

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

        # Memoized callback - only recreated if todos changes
        handleToggle = useCallback(lambda id: str -> None {
            setTodos(todos.map(lambda todo: any -> any {
                if todo._jac_id == id {
                    return {...todo, "done": not todo.done};
                }
                return todo;
            }));
        }, [todos]);

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

    def app() -> any {
        return <TodoApp />;
    }
}

State Management Patterns#

Pattern 1: Action Functions#

Encapsulate state logic in reusable action functions:

cl import from react { useState }

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

        # Action functions
        async def addTodo(text: str) -> None {
            if not text.trim() { return; }
            response = root spawn create_todo(text=text);
            new_todo = response.reports[0][0];
            setTodos(todos.concat([new_todo]));
        }

        async def toggleTodo(id: str) -> None {
            id spawn toggle_todo();
            setTodos(todos.map(lambda todo: any -> any {
                if todo._jac_id == id {
                    return {...todo, "done": not todo.done};
                }
                return todo;
            }));
        }

        def removeTodo(id: str) -> None {
            setTodos(todos.filter(lambda todo: any -> bool {
                return todo._jac_id != id;
            }));
        }

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

        return <div>
            {/* UI using these actions */}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Pattern 2: Selector Functions with useMemo#

Create memoized selector functions for derived data:

cl import from react { useState, useMemo }

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

        # Memoized selectors
        filteredTodos = useMemo(lambda -> list {
            if filter == "active" {
                return todos.filter(lambda t: any -> bool { return not t.done; });
            } elif filter == "completed" {
                return todos.filter(lambda t: any -> bool { return t.done; });
            }
            return todos;
        }, [todos, filter]);

        activeTodos = useMemo(lambda -> list {
            return todos.filter(lambda t: any -> bool { return not t.done; });
        }, [todos]);

        completedTodos = useMemo(lambda -> list {
            return todos.filter(lambda t: any -> bool { return t.done; });
        }, [todos]);

        return <div>
            <div>Active: {activeTodos.length}</div>
            <div>Completed: {completedTodos.length}</div>
            {filteredTodos.map(lambda todo: any -> any {
                return <TodoItem key={todo._jac_id} todo={todo} />;
            })}
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Pattern 3: Combining Multiple Hooks#

Combine useState, useReducer, and useContext for complex applications:

cl import from react { useState, useReducer, useContext, createContext, useEffect }

cl {
    # Context for global state
    AppContext = createContext(None);

    # Main app with combined hooks
    def App() -> any {
        # User state with useState
        let [user, setUser] = useState(None);

        # Todo state with useReducer
        def todoReducer(state: dict, action: dict) -> dict {
            if action.type == "ADD" {
                return {...state, "todos": state.todos.concat([action.payload])};
            } elif action.type == "TOGGLE" {
                return {
                    ...state,
                    "todos": state.todos.map(lambda t: any -> any {
                        if t._jac_id == action.payload {
                            return {...t, "done": not t.done};
                        }
                        return t;
                    })
                };
            }
            return state;
        }

        let [todoState, dispatch] = useReducer(todoReducer, {"todos": [], "loading": False});

        # UI state with useState
        let [theme, setTheme] = useState("light");

        useEffect(lambda -> None {
            async def loadData() -> None {
                userData = root spawn get_user();
                setUser(userData);
            }
            loadData();
        }, []);

        contextValue = {
            "user": user,
            "setUser": setUser,
            "todoState": todoState,
            "dispatch": dispatch,
            "theme": theme,
            "setTheme": setTheme
        };

        return <AppContext.Provider value={contextValue}>
            <TodoList />
        </AppContext.Provider>;
    }

    def app() -> any {
        return <App />;
    }
}

Here's a complete example combining multiple React hooks and patterns:

cl import from react { useState, useEffect, useMemo, useCallback }

cl {
    def TodoApp() -> any {
        # State management
        let [todos, setTodos] = useState([]);
        let [filter, setFilter] = useState("all");
        let [loading, setLoading] = useState(False);
        let [error, setError] = useState(None);
        let [user, setUser] = useState(None);
        let [sidebarOpen, setSidebarOpen] = useState(False);

        # Load initial data
        useEffect(lambda -> None {
            async def loadData() -> None {
                setLoading(True);
                setError(None);
                try {
                    # Load user and todos in parallel
                    results = await Promise.all([
                        root spawn get_current_user(),
                        root spawn read_todos()
                    ]);
                    setUser(results[0]);
                    setTodos(results[1].reports);
                } catch (err) {
                    setError(err);
                } finally {
                    setLoading(False);
                }
            }
            loadData();
        }, []);

        # Memoized derived state
        filteredTodos = useMemo(lambda -> list {
            if filter == "active" {
                return todos.filter(lambda t: any -> bool { return not t.done; });
            } elif filter == "completed" {
                return todos.filter(lambda t: any -> bool { return t.done; });
            }
            return todos;
        }, [todos, filter]);

        stats = useMemo(lambda -> dict {
            total = todos.length;
            active = todos.filter(lambda t: any -> bool { return not t.done; }).length;
            return {"total": total, "active": active, "completed": total - active};
        }, [todos]);

        # Memoized action functions
        addTodo = useCallback(lambda text: str -> None {
            async def _addTodo() -> None {
                response = root spawn create_todo(text=text);
                new_todo = response.reports[0][0];
                setTodos(todos.concat([new_todo]));
            }
            _addTodo();
        }, [todos]);

        toggleTodo = useCallback(lambda id: str -> None {
            async def _toggleTodo() -> None {
                id spawn toggle_todo();
                setTodos(todos.map(lambda t: any -> any {
                    if t._jac_id == id {
                        return {...t, "done": not t.done};
                    }
                    return t;
                }));
            }
            _toggleTodo();
        }, [todos]);

        removeTodo = useCallback(lambda id: str -> None {
            setTodos(todos.filter(lambda t: any -> bool { return t._jac_id != id; }));
        }, [todos]);

        clearCompleted = useCallback(lambda -> None {
            setTodos(todos.filter(lambda t: any -> bool { return not t.done; }));
        }, [todos]);

        toggleSidebar = useCallback(lambda -> None {
            setSidebarOpen(not sidebarOpen);
        }, [sidebarOpen]);

        # Render
        if loading {
            return <div style={{"padding": "20px"}}>Loading...</div>;
        }

        if error {
            return <div style={{"padding": "20px", "color": "red"}}>
                Error: {error}
            </div>;
        }

        return <div style={{"display": "flex", "minHeight": "100vh"}}>
            # Sidebar
            {sidebarOpen and <div style={{"width": "250px", "padding": "20px", "background": "#f5f5f5"}}>
                <h3>Filter</h3>
                <button onClick={lambda -> None { setFilter("all"); }}>All ({stats.total})</button>
                <button onClick={lambda -> None { setFilter("active"); }}>Active ({stats.active})</button>
                <button onClick={lambda -> None { setFilter("completed"); }}>Completed ({stats.completed})</button>
            </div>}

            # Main content
            <div style={{"flex": "1", "padding": "20px"}}>
                # Header
                <div style={{"display": "flex", "justifyContent": "space-between", "marginBottom": "20px"}}>
                    <h1>Welcome, {user.name if user else "Guest"}</h1>
                    <button onClick={toggleSidebar}>
                        {"Hide" if sidebarOpen else "Show"} Sidebar
                    </button>
                </div>

                # Stats
                <div style={{"marginBottom": "20px"}}>
                    {stats.active} active, {stats.completed} completed, {stats.total} total
                </div>

                # Todo list
                <div>
                    {filteredTodos.map(lambda todo: any -> any {
                        return <div key={todo._jac_id} style={{"marginBottom": "10px"}}>
                            <input
                                type="checkbox"
                                checked={todo.done}
                                onChange={lambda -> None { toggleTodo(todo._jac_id); }}
                            />
                            <span style={{"marginLeft": "10px"}}>{todo.text}</span>
                            <button
                                onClick={lambda -> None { removeTodo(todo._jac_id); }}
                                style={{"marginLeft": "10px"}}
                            >
                                Delete
                            </button>
                        </div>;
                    })}
                </div>

                # Clear completed button
                {stats.completed > 0 and <button
                    onClick={clearCompleted}
                    style={{"marginTop": "20px"}}
                >
                    Clear Completed
                </button>}
            </div>
        </div>;
    }

    def app() -> any {
        return <TodoApp />;
    }
}

Best Practices#

1. Separate State Variables by Concern#

cl import from react { useState }

#  Good: Separate state variables
def App() -> any {
    let [user, setUser] = useState(None);
    let [todos, setTodos] = useState([]);
    let [sidebarOpen, setSidebarOpen] = useState(False);
    let [theme, setTheme] = useState("light");
}

#  Avoid: One giant state object for unrelated data
def App() -> any {
    let [appState, setAppState] = useState({
        "user": None,
        "todos": [],
        "sidebarOpen": False,
        "theme": "light"
    });
}

2. Use useMemo for Expensive Computations#

cl import from react { useState, useMemo }

#  Good: Memoize expensive calculations
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);

    activeTodos = useMemo(lambda -> list {
        return todos.filter(lambda t: any -> bool { return not t.done; });
    }, [todos]);
}

#  Avoid: Computing on every render
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);

    # This runs on every render, even if todos hasn't changed
    activeTodos = todos.filter(lambda t: any -> bool { return not t.done; });
}

3. Don't Store Derived State#

cl import from react { useState }

#  Good: Calculate derived values
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);
    activeCount = todos.filter(lambda t: any -> bool { return not t.done; }).length;
}

#  Avoid: Storing derived values in state
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);
    let [activeCount, setActiveCount] = useState(0);  # Redundant!
}

4. Use useReducer for Complex State Logic#

cl import from react { useReducer }

#  Good: useReducer for complex interdependent state
def TodoApp() -> any {
    def reducer(state: dict, action: dict) -> dict {
        if action.type == "ADD" {
            return {...state, "todos": state.todos.concat([action.payload]), "count": state.count + 1};
        }
        return state;
    }

    let [state, dispatch] = useReducer(reducer, {"todos": [], "count": 0});
}

#  Avoid: Multiple useState for interdependent state
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);
    let [count, setCount] = useState(0);
    # Risk of inconsistency - need to update both together
}

5. Always Handle Loading and Error States#

cl import from react { useState, useEffect }

#  Good: Comprehensive state management
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);
    let [loading, setLoading] = useState(False);
    let [error, setError] = useState(None);

    useEffect(lambda -> None {
        async def loadTodos() -> None {
            setLoading(True);
            setError(None);
            try {
                result = root spawn read_todos();
                setTodos(result.reports);
            } catch (err) {
                setError(err);
            } finally {
                setLoading(False);
            }
        }
        loadTodos();
    }, []);

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

6. Use useCallback for Callbacks Passed to Children#

cl import from react { useState, useCallback }

#  Good: Memoized callbacks prevent unnecessary re-renders
def TodoApp() -> any {
    let [todos, setTodos] = useState([]);

    handleToggle = useCallback(lambda id: str -> None {
        setTodos(todos.map(lambda t: any -> any {
            if t._jac_id == id { return {...t, "done": not t.done}; }
            return t;
        }));
    }, [todos]);

    return <TodoList todos={todos} onToggle={handleToggle} />;
}

7. Use Context for Deeply Nested Props#

cl import from react { useState, useContext, createContext }

#  Good: Context avoids prop drilling
ThemeContext = createContext("light");

def App() -> any {
    let [theme, setTheme] = useState("light");
    return <ThemeContext.Provider value={theme}>
        <DeeplyNestedComponent />
    </ThemeContext.Provider>;
}

def DeeplyNestedComponent() -> any {
    theme = useContext(ThemeContext);
    return <div style={{"background": theme}}></div>;
}

Summary#

  • useState: Use for simple, independent state variables
  • useReducer: Use for complex, interdependent state logic
  • useContext: Use for global state and avoiding prop drilling
  • useMemo: Use to memoize expensive computations
  • useCallback: Use to memoize callbacks passed to child components
  • Custom Hooks: Create reusable state logic
  • Best Practices: Separate concerns, avoid derived state, handle errors

React hooks provide a powerful and flexible way to manage state in Jac applications!