Skip to main content

How i have integrated react-flow with complex deep nested object data manipuation

· 4 min read
Lucas Sovre
Software architect, Docker certified expert, cloud and devsecops .

This article is the following of the article "How to create a nextjs + mongoDB SaaS fully self-hosted"

We will mainly focus on how does the magic happen inside the NextJs App ?

The requirements :

What we want to build is a nodal GUI handler for docker-compose data handling.

From this product need we can extract somes technicals answers :

  • We need a good data handling of docker-compose
  • We need a tool to help us with the nodal GUI
  • We need a state management sytem that allow us complex deep nested object mutations, in an efficient way.

Choosing a data library to handle docker-compose efficiently

Docker-compose is a really complex data structre, as it's deeply nested and somes bottoms objects are linked to top ones. I looked for any docker-compose but didn't find any in TS, so i builded mine from the ground.

It's curently not open-source but we do have a form to ask acces to it

The library work with holding all the states inside a big Compose class. managing the state of just one Compose instance, it becomes straightforward to stay up-to-date with all job-related data.

React state management

From the start we knew we have to manage the Compose state on client side as there is a lot of state mutation made on the playground page.

But, what to choose between React UseState, Zustand, Recoil, Redux... ?

We chose Zustand for our state management because of its seamless integration with Next.js and its ability to modify state by passing a function instead of a new object.

Why does this matter? In the traditional approach, using setData(newData) would recreate the entire Compose object on every state update, like this:

const [compose,setCompose] = useComposeState()

setCompose(new Compose(whatever you change))

This would result in a new instance of Compose every time, which could lead to inefficiencies or unnecessary re-renders.

Instead, Zustand allows us to pass a modifier function, enabling changes to be applied directly to the existing object without creating a new instance:

const [compose,setCompose] = useComposeState()

setCompose((oldCompose)=>oldCompose.whateverChange(anyParamsNeeded))

This approach allows targeted updates and keeps the instance intact. For more granular changes, you can directly modify properties within the state:

const [compose,setCompose] = useComposeState()

setCompose((oldCompose)=>{
oldCompose.service1.name = "new name"
})

By using this method, we maintain the simplicity and efficiency of state updates without compromising the integrity of the Compose instance

But you maybe wonder, what does the zustand store look like ?

"use client";

import { create } from "zustand";
import { Compose } from "@entrecompetents/composecraftLib";
import { generateRandomName } from "@/lib/utils";

interface ComposeState {
compose: Compose;
setCompose: (updater: (currentCompose: Compose) => void) => void;
}

export const useComposeStore = create<ComposeState>((set) => ({
compose: new Compose({ name: generateRandomName() }),
setCompose: (updater: (currentCompose: Compose) => void) => {
set((state) => {
updater(state.compose);
return { compose: state.compose };
});
},
}));

The nodal GUI library

The options was : JointJS, react flow, mermaidJS (their was maybe other options but we didn't go further in the searchs)

Here is a small recap table of our choose :

JointJSReact FlowmermaidJS
Interactivity
Easy to dev⭐️?⭐️⭐️⭐️
Open source

We finally choosed React flow for the interactivity and the fact it's open source.

But keep in mind it's not an easy way to go, as react-flow require a lot of custom dev.

The playground scheme :

scheme

Conclusion :

Building a nodal GUI for handling Docker Compose data in a Next.js application is a multi-layered process that combines efficient state management, a robust GUI library, and custom-built tooling. Through this exploration, we have:

  1. Crafted a TypeScript library for managing Docker Compose data that suits the complexity and nested nature of the structure.
  2. Optimized client-side state management using Zustand, allowing for efficient updates without recreating entire objects or instances.
  3. Selected React Flow as the nodal GUI library, leveraging its interactivity and customizability while tackling the steep learning curve through targeted development.

This integration, represented in the final scheme, showcases a streamlined workflow where deeply nested state changes, GUI interactions, and backend communication seamlessly come together. The chosen tools not only meet the functional requirements but also align with performance, usability, and scalability goals.

These efforts culminate in a playground page that delivers a cohesive, interactive user experience while adhering to technical best practices.