Published on

The Journey to Better Architecture

Authors
  • avatar
    Name
    Igor Tosic
    Twitter

A Personal Journey into State Management

The Journey to Better Architecture

As a developer currently working on a complex canvas-based annotation system, I find myself exploring and reimagining better architectural patterns. This ongoing experience has inspired the creation of EasyNotes Canvas, which serves as a proving ground for implementing these architectural improvements. This is a story about that journey, and the insights being gained along the way.

The Birth of EasyNotes Canvas

EasyNotes Canvas emerged from a simple yet profound realization: the tools we use for digital note-taking and annotation should feel as natural as pen and paper, yet harness the full power of modern web technologies. Built with React, Redux, and Konva, it serves as a canvas for thoughts, ideas, and collaborative work.

But beyond its functional purpose, EasyNotes Canvas represents something more: a quest for architectural elegance in the face of complexity.

The State Management Epiphany

Wrestling with Complexity

My initial attempts at managing canvas state were, frankly, chaotic. Local state scattered across components, prop drilling that made code reviews painful, and event handlers that grew more complex with each new feature. It was a classic case of organic growth without architectural foresight.

// Early attempts at state management - a lesson in what not to do
const CanvasComponent = () => {
  const [shapes, setShapes] = useState([]);
  const [selectedShape, setSelectedShape] = useState(null);
  const [isDrawing, setIsDrawing] = useState(false);
  // More state variables...
}

The turning point came when I realized that canvas applications aren't just about drawing —they're about managing a complex ecosystem of states, events, and user interactions. This led to a fundamental shift in thinking about state management.

The Redux Revelation

Moving to Redux wasn't just about choosing a state management library; it was about adopting a philosophy of predictable state changes and clear data flow. The resulting architecture became a testament to this philosophy:

interface ShapeState {
  shapes: ShapeProps[];
  savedShapes: ShapeProps[];
  isDirty: boolean;
  selectedShapeId: string | null;
  activeTool: ShapeType | string | null;
  // A clear representation of our application's state
}

Each property in this interface tells a story about user interaction and application behavior. The isDirty flag, for instance, represents the tension between user changes and persistence—a simple boolean that carries significant meaning for the user experience.

While React's Context API and useReducer hook might seem like suitable alternatives—and they are excellent for simpler applications—the decision to use Redux came from deeper architectural needs. Context API, while powerful for prop drilling prevention, lacks the robust middleware ecosystem that Redux provides for handling complex side effects. The useReducer hook, though similar in concept to Redux, doesn't offer the same level of state isolation and time-travel debugging capabilities that become invaluable when debugging complex canvas interactions. In a canvas application where every mouse movement could trigger state changes and where undo/redo functionality is crucial, Redux's DevTools and middleware system prove themselves indispensable for development and debugging.

The Art of Event Handling

Finding Harmony Between Events and State

One of the most enlightening aspects of this journey was discovering the delicate balance between immediate user feedback and state consistency. Mouse events in canvas applications aren't just about capturing coordinates; they're about translating human intent into digital creation.

export const useCanvasEvents = (handleShapeAdd, handleShapeUpdate) => {
  const handleMouseDown = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
    // This moment of interaction initiates a cascade of state changes
    const clickedOnShape = e.target instanceof Konva.Shape;
    if (clickedOnShape) {
      dispatch(selectShape(e.target.id()));
      return;
    }
    // The beginning of creation...
  }, [dispatch]);

This code represents more than just event handling—it's the bridge between user intention and application state. Each mouse movement tells a story, and our architecture must be ready to listen and respond.

Lessons in Architecture

The Value of Clarity

Working on my current canvas project has taught me the importance of architectural clarity. When you're dealing with complex interactions and state changes, your architecture needs to be more than just functional—it needs to be comprehensible. This led to some key principles in EasyNotes Canvas:

1. State as a Source of Truth: Every shape, every tool selection, every user interaction is reflected in our Redux store. This isn't just about state management—it's about creating a clear narrative of application state.

2. Event Handling as Translation: Mouse events become a translation layer between user intent and application state. This perspective changed how I approached event handling:

const handleShapeModification = async (shapeId, newProperties) => {
  // Immediate feedback for the user
  dispatch(updateShape({ id: shapeId, ...newProperties }));

  try {
    // Ensuring our changes persist
    await dispatch(saveShapes(currentProjectId)).unwrap();
  } catch (error) {
    // Gracefully handling when things go wrong
    dispatch(revertShapeUpdate(shapeId));
  }
};

Performance as a Feature

One of the most valuable lessons from my project was that performance isn't just about optimization—it's about user experience. This led to implementations like batch operations and memoized selectors:

export const selectSelectedShape = createSelector(
  [(state) => state.shapes.shapes, (state) => state.shapes.selectedShapeId],
  (shapes, selectedId) => shapes.find(shape => shape.id === selectedId)
);

Looking Forward

The journey of building EasyNotes Canvas has been about applying lessons learned in real-time from my current work with canvas applications. Each architectural decision carries the weight of active experiences and challenges, and each line of code represents a step toward better software design.

Future enhancements like PDF integration and collaborative editing aren't just features to be added—they're opportunities to further refine our architectural approach. The foundation we've built, inspired by real-world experiences and challenges, stands ready for these additions.

Conclusion

Building canvas applications has taught me that good architecture isn't just about patterns and practices—it's about understanding the deeper relationships between user interactions, application state, and system behavior. EasyNotes Canvas is more than just an application; it's a reflection of this understanding, and a step forward in my journey as a developer.


Next in this series: "Beyond Drawing - Implementing Document Integration in Canvas Applications," where we'll explore the architectural challenges of adding PDF and document support to canvas applications.

People Vectors by Vecteezy