Week 5 - Refactoring components
Summary
This week, I continued refactoring the diagram editor to properly implement composition on the frontend. This required rewriting related components and simplifying the overall logic. As part of this effort, I refactored the RB communications wrapper, created a library of API wrappers, and moved much of the functionality of the diagram editor to the Node Header component. All refactored components are now written in TypeScript, which has significantly improved overall code quality, as noted in this issue.
The RB communication manager
This is a wrapper that handles communication with the dockerized RB via web sockets. It provides functions to send messages and manage RB responses using promises.
Since it wasn’t a React component, I moved it to a new section of the app called api_helper
. Previously, it was a collection of functions, but to better manage its state and enable use across different React components, I converted it into a class. I implemented it using a well-structured singleton pattern, ensuring that communication is initialized only once and is accessible from any React component as needed.
Creating an API wrapper
There are two types of communication in BT Studio: using WebSockets with the RB, as explained above, and using HTTP with the backend REST API (called tree_api
).
Previously, the implementation of functions to call the HTTP API was scattered across different components and even passed as props between them. This approach led to inconsistencies and made these functions difficult to maintain and improve. Additionally, they were tightly coupled to specific components, unnecessarily complicating each component’s business logic.
Now, I’m progressively moving all this functions into a wrapper library, called TreeWrapper.ts
. This ensures the calls are made in the same way in all the components and greatly improves code reusability.
For reference, this is the implementation of the project creation wrapper.
const createProject = async (projectName: string) => {
if (!projectName.trim()) {
throw new Error("Project name cannot be empty.");
}
const apiUrl = `/tree_api/create_project?project_name=${encodeURIComponent(projectName)}`;
try {
const response = await axios.get(apiUrl);
// Handle unsuccessful response status (e.g., non-2xx status)
if (!response.data.success) {
throw new Error(response.data.message || "Failed to create project."); // Response error
}
} catch (error: unknown) {
throw error; // Rethrow
}
};
While reimplementing these functions, I focused heavily on error management. Previously, these functions included console logging and had various side effects, leading to inconsistent behavior. Now, all functions follow a consistent structure:
- Parameter Validation: Check the validity of input parameters and throw a specific error if they are invalid.
- API Call Execution: Attempt to call the API URL using valid parameters, while catching any client-side errors that occur during the call.
- Error Handling for API Responses: If the API response indicates an error (a server-side error), throw a clear and descriptive error.
- Return a Promise: If the response is successful, the functions return a promise (since they are async), which can be easily awaited by the caller function.
With this structure, the functions are highly organized and handle all types of errors in a clear and consistent manner. Note that the error reporting now is left up to the calling function, through trycath
statements. For example:
const onSaveProject = async () => {
try {
await saveProject(modelJson, currentProjectname);
setProjectChanges(false);
console.log("Project saved");
} catch (error) {
if (error instanceof Error) {
console.error("Error saving project: " + error.message);
}
}
};
Note that even the error handling is completely type-safe now. Thanks Typescript!
This approach not only makes error reporting more comprehensive but also allows the functions to be imported and used anywhere without needing to pass them around as props.
The new Header Menu
With this collection of API libraries, I began migrating all excessive functionality from the Diagram Editor to the Header Menu, resulting in a complete reimplementation (except for the modals). I also rewrote the component in TypeScript, ensuring it adheres to the principles of component purity. Now, this component manages the application execution and interacts with the API in a standardized way.
Next steps
Next week, I plan to create nested instances of the diagram editor (now that it is a pure component), enabling effective composition on the frontend. With this capability, I will update the get_project_graph
endpoint to support subgraphs as explained in previous weeks and complete the additional step in the app_generator: the XML merger.