DEV Community

Cover image for Integrate React component into Solid JS (with Typescript) (part 2)
TuanNQ
TuanNQ

Posted on

Integrate React component into Solid JS (with Typescript) (part 2)

In part 1, we can now render React component inside a Solid application. In part 2 we'll tackle the communication problem: how to pass data between 2 applications.

The communication problem consists of 2 smaller problems:

  • Communicate from React component to the main Solid application
  • Communicate from the Solid main application to React component

In a normal "pure" React or Solid application, we use props to pass data between components. But we can't use that normal way to communicate between 2 applications. Instead, we use the good old callback.

You can check the source code here on Github for this article.

Let's tackle the first problem: communicate from the React component up to the main Solid application.

Communicate from React component to the main Solid application

First, let's build the classic counter application: the React component will render a button that each time it get clicked the counter in the Solid main application will increment by 1.

React in Solid counter app

In order to do that, the React component need to tell the Solid main application each time the button get clicked. The Solid main application will hand the React component a function onIncrement. Each time the React component want to increment, it'll call that function.

React hand Solid a function

React component call Solid's function

Here's the detail implementation:

  • The mount now accepts a new argument: a function called onIncrement
  • The main Solid app pass a function onIncrement to the mount function
  • The mount receive that function add pass it to the App component
  • Each time the App component need to increment, it'll call that function

React communicate with main Solid application

React communicate with main Solid application when user click

Let's dive into the coding part! In the App.tsx of the React component add the following:

// App.tsx (React app)
interface Props {
  onIncrement: (amount: number) => any;
}

export const App: React.FC<Props> = ({ onIncrement }) => {
  return <button onClick={() => onIncrement(1)}>Increment from React</button>;
};
Enter fullscreen mode Exit fullscreen mode

The App component receive the onIncrement function. Each the the button get click it'll call the onIncrement with the value 1.

In the index.tsx file (of the React component) change to the following:

// index.tsx (React app)
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";

interface Callbacks {
  onIncrement: (amount: number) => any;
}

export const mount = (root: HTMLElement, { onIncrement }: Callbacks) => {
  createRoot(root).render(
    <StrictMode>
      <App onIncrement={onIncrement} />
    </StrictMode>
  );
};
Enter fullscreen mode Exit fullscreen mode

The mount function receive the onIncrement function and pass it to the App component.

Now, let's run the build script again in the react-component folder:

pnpm run build
Enter fullscreen mode Exit fullscreen mode

Next, in the App.tsx in the Solid application add the following:

// App.tsx (Solid app)
import { mount } from "react-component";
import { createSignal, onMount } from "solid-js";

export const App = () => {
  let container!: HTMLDivElement;

  const [value, setValue] = createSignal<number>(0);

  onMount(() => {
    mount(container, {
      onIncrement: (amount) => {
        setValue((value) => value + amount);
      },
    });
  });

  return (
    <>
      <div ref={container} />
      <div>
        <div>Count from Solid: {value()}</div>
      </div>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

The main Solid application create a function called onIncrement with the amount parameter. Each time it get called Solid will increment the value in the state with that amount.

Now, if you click the "Increment from React" button, the counter in the Solid application will increment by 1!

Communicate from the main Solid application to the React component

Next, let's extends the application to have an input in the Solid application which tell the React component the amount it should increment.

React in Solid more complex counter app

Similar to the way React component communicate with the Solid application above, the React component will hand the main Solid application a function setAmount. Each time the value in the input change the main Solid application will call that function.

React hand Solid a function

Solid app call React component's function

Here's the detail implementation:

  • The mount function returns a setAmount function
  • The Solid main application receive that setAmount function
  • Each time the input change, Solid main application will call that setAmount function

Main Solid application communicate with React component

Main Solid application communicate with React component when user input change

In the App.tsx in the Solid main application, change to the following:

// App.tsx (Solid app)
import { mount, type ReturnedCallbacks } from "react-component";
import { createSignal, onMount } from "solid-js";

export const App = () => {
  let container!: HTMLDivElement;

  let setAmount: ReturnedCallbacks["setAmount"] | undefined;

  const [value, setValue] = createSignal<number>(0);

  onMount(() => {
    const callbacks = mount(container, {
      onIncrement: (amount) => {
        setValue((value) => value + amount);
      },
    });

    setAmount = callbacks.setAmount;
  });

  return (
    <>
      <div ref={container} />
      
      <div>
        <input onInput={(e) => {
            const value = parseFloat(e.currentTarget.value);

            if (!isNaN(value)) {
              setAmount && setAmount(value);
            }
          }}
        />

        <div>Count from Solid: {value()}</div>
      </div>
    </>
  );

};
Enter fullscreen mode Exit fullscreen mode

First, we get a new function called setAmount from the mount function. Then we set it to the setAmount function defined at the top of the component. Now we can call setAmount anywhere in our component.

Above the counter from solid div, we add an input that each time user change, it'll get the value from the input and call setAmount function.

*Note: on Solid JS, we don't use the onChange event listener but instead use onInput

In the index.tsx of the React component, change to the following:

// src/index.tsx (React component)
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";

export interface Callbacks {
  onIncrement: (amount: number) => any;
}

export interface ReturnedCallbacks {
  setAmount: (amount: number) => any;
}

export const mount = (
  root: HTMLElement,
  { onIncrement }: Callbacks
): ReturnedCallbacks => {
  createRoot(root).render(
    <StrictMode>
        <App onIncrement={onIncrement} />
    </StrictMode>
  );

  return {
    setAmount: (amount: number) => {
      // TODO: pass the amount to the App component
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

The mount function now return a setAmount function for the main Solid application to call when needed.

But how can we pass the amount into the App component? In order to do that, we'll need to use a store management solution (like Redux). Because in essence, we need to pass a value into the whole React application.

There are many store management solution out there like Redux, MobX, Nano Stores, Valtio,... If you want a minimal solution, you might want to choose a simple solution like Nano Stores,.. but in this article, for the sake of familiarity, I'll use Redux.

Here's a full diagram with Redux:

Main Solid application with React component input change into Redux

First, let's install Redux. Remember to run this command in the React component project.

pnpm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

In the src folder of the React component, create amount-slice.ts with the following content:

// src/amount-slice.ts (React component)
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "./store";

// Define a type for the slice state
export interface AmountState {
  value: number;
}

// Define the initial state using that type
const initialState: AmountState = {
  value: 0,
};

const amountSlice = createSlice({
  name: "amount",
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    setAmount: (state, action: PayloadAction<number>) => {
      state.value = action.payload;
    },
  },
});

export const { setAmount } = amountSlice.actions;

// Other code such as selectors can use the imported `RootState` type
export const selectAmount = (state: RootState) => state.amount.value;

export default amountSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Next, in the src folder of the React component, create store.ts file with the following content:

// src/store.ts (React component)
import {
  configureStore,
  EnhancedStore,
  StoreEnhancer,
  ThunkDispatch,
  Tuple,
  UnknownAction,
} from "@reduxjs/toolkit";
import amountSlice, { AmountState } from "./amount-slice";

export const store: Store = configureStore({
  reducer: {
    amount: amountSlice,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;

export type Store = EnhancedStore<
  {
    amount: AmountState;
  },
  UnknownAction,
  Tuple<
    [
      StoreEnhancer<{
        dispatch: ThunkDispatch<
          {
            amount: AmountState;
          },
          undefined,
          UnknownAction
        >;
      }>,
      StoreEnhancer
    ]
  >
>;
Enter fullscreen mode Exit fullscreen mode

Now, we can wire it together in the index.tsx:

// src/index.tsx (React component)
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { Provider } from "react-redux";
import { store } from "./store";
import { setAmount } from "./amount-slice";

export interface Callbacks {
  onIncrement: (amount: number) => any;
}

export interface ReturnedCallbacks {
  setAmount: (amount: number) => any;
}

export const mount = (
  root: HTMLElement,
  { onIncrement }: Callbacks
): ReturnedCallbacks => {
  createRoot(root).render(
    <StrictMode>
      <Provider store={store}>
        <App onIncrement={onIncrement} />
      </Provider>
    </StrictMode>
  );

  return {
    setAmount: (amount: number) => {
      store.dispatch(setAmount(amount));
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

In order to make changes to the store outside of a React component, you can use store.dispatch().

Next, in the App.tsx file of the React component we will get the amount that stored in Redux:

// src/App.tsx (React component)
import { useSelector } from "react-redux";
import { RootState } from "./store";

interface Props {
  onIncrement: (amount: number) => any;
}

export const App: React.FC<Props> = ({ onIncrement }) => {
  const amount = useSelector((state: RootState) => state.amount.value);

  return (
    <button onClick={() => onIncrement(amount)}>
      Increment {amount} from React
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's build the React component library:

pnpm run build
Enter fullscreen mode Exit fullscreen mode

Now, if you run the server, you'll see the following in the screen:

Application screenshot

The application is complete!

Conclusion

Now you know about all the nitty gritty technical details how to integrate a React component into a Solid JS application.

But I think there's still a lot of work ahead to actually create your own custom solution for your application. So in the next part I'll talk more about the building the React component part: explaining basic settings and configurations you might want to know.

Top comments (0)