DEV Community

Altin Selimi
Altin Selimi

Posted on

1

Creating a library for both React and Vue 🤝

Getting to re-know an old friend đź«‚

A while ago, I started developing with React (again). Coming from Vue, it was quite a shift in terms of building frontend UIs.

Parent-to-child data flow, component rendering intricacies, lifecycle hooks, and context for state management to avoid prop drilling—it was a lot to grasp just from reading documentation.

So, I decided that my next library—whenever that happened—would be built to support both Vue and React. It should give me a much better understanding on direct differences between the two.

The Idea: A Panes library 🪟

I really like Linear’s user experience—it's intuitive yet unobtrusive. You might never think to drag the panes, but if you try, it just works. Resize the window? The right pane collapses automatically. "It just works."

So I decided that's what I'm going to build, a panes library that gives you the tools to build such a dashboard. Which is now TurtlePanes.

Initial Exploration đź‘€

I liked React’s Context API—it prevents prop drilling and serves as a shared state. Perfect.

So the blueprint was simple: composable components that relied on a shared context. In theory, a great idea.

I started with Vue, and the initial implementation was quick, but I was writing too much business logic inside Vue components. Eventually, I moved most logic into the context object, keeping the components clean.

The Vue version was working great, but then I hit a major roadblock while porting it to React.

Vue vs. React Reactivity Differences âš–️

Vue has a reactive function that transforms an object into a reactive one, allowing the framework to track changes and trigger re-renders. React, however, handles reactivity differently. A state variable must be updated using its corresponding setter function—otherwise, no re-render occurs.

The JavaScript object itself updates, but React won’t reflect the updates.

const [sharedState, setSharedState] = useState({ count: 1 });
sharedState.count++; // 🙅🏻‍♂️ Updates the object, but won't trigger a re-render
setSharedState((prev) => ({ ...prev, count: prev.count + 1 })); // âś… Correct approach
Enter fullscreen mode Exit fullscreen mode

I explored libraries like Valtio and Mitosis, but neither fit perfectly. I didn’t want to add a peer dependency for Valtio, and Mitosis didn’t work well with a shared context.

Instead of maintaining separate contexts for Vue and React, I decided to use a single core state and “hack” React’s update mechanism by listening for changes on a JavaScript object.

The Solution: A Shared Proxy-Based State đź’ˇ

The core state provides methods to create a state object and a proxy wrapper that listens for changes, triggering updates as needed. (As a bonus, Vue’s reactivity mechanism actually works in a similar way, using Proxies to track changes and update the UI automatically.)

// context.ts
export const createState = () => ({ count: 0 });
export const createActions = (state) => ({
    updateCount: () => state.count++
});
export const createProxyState = (state, onUpdateCallback) => {
    return new Proxy(state, {
        set(target, prop, value) {
            target[prop] = value;
            onUpdateCallback();
        }
    });
};
Enter fullscreen mode Exit fullscreen mode

With these three functions, we should now have everything to share the state between the two.

Vue Adapter

Now we need to create a Vue adapter, which uses 'reactive' hook to turn it into a digestable object for Vue components.

// vue-state-adapter.ts
import { createState, createActions } from './context.ts';
import { reactive, provide, inject } from 'vue';

export const createContext = () => {
  const state = reactive(createState());
  const actions = createActions(state);
  return { state, ...actions };
};
Enter fullscreen mode Exit fullscreen mode
<script>
// Usage in a Vue component
import { createContext } from "./vue-state-adapter";
const context = createContext();
provide("context", context);
</script>

<template>
  <div>
    <p>Count: {{ context.state.count }}</p>
    <button @click="context.updateCount">Increment</button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

React Adapter

And likewise for React, we need to make sure the UI updates everytime something changes in the proxied state.

// react-state-adapter.tsx
import { createContext, useRef, useState, useMemo, useContext } from "react";
import { createState, createProxyState, createActions } from "@turtle-panes/core";

const StateContext = createContext(undefined);

export const StateProvider = ({ children }) => {
  const [triggerRerender, setTriggerRerender] = useState(0);
  const state = useRef(null);
  if (!state.current) {
    const initialState = createState();
    state.current = createProxyState(initialState, () => setTriggerRerender((prev) => prev + 1));
  }
  const actions = useRef(null);
  if (!actions.current) {
    actions.current = createActions(state.current);
  }

  const contextValue = useMemo(() => {
    return { state: state.current, ...actions.current };
  }, [triggerRerender]);

  return <StateContext.Provider value={contextValue}>{children}</StateContext.Provider>;
};

export const useStateContext = () => {
  const context = useContext(StateContext);
  if (context === undefined) {
    throw new Error("useStateContext must be used within a StateProvider");
  }
  return context;
};
Enter fullscreen mode Exit fullscreen mode

Using It in a React Component

import { StateProvider, useStateContext } from "./react-state-adapter";

const Counter = () => {
  const { state, updateCount } = useStateContext();
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={updateCount}>Increment</button>
    </div>
  );
};

const App = () => (
  <StateProvider>
    <Counter />
  </StateProvider>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion 🙆🏻‍♂️

This approach provides a framework-agnostic way to manage state across React and Vue while keeping a unified core. The proxy trick ensures React detects state changes without requiring additional dependencies. The result? A lightweight, scalable solution that “just works.”

I look forward to building more libraries like this, and maybe even start with a core library that uses vanilla JS, and then have framework adapters built around that instead.

Let me know if there's something I missed, or some feedback you might have, whatever it may be.

You can find the library here: http://github.com/altinselimi/turtle-panes

Top comments (0)

đź‘‹ Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay