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.
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.
Here's the detail implementation:
- The
mountnow accepts a new argument: a function calledonIncrement - The main Solid app pass a function
onIncrementto themountfunction - The
mountreceive that function add pass it to the App component - Each time the App component need to increment, it'll call that function
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>;
};
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>
);
};
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
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>
</>
);
};
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.
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.
Here's the detail implementation:
- The
mountfunction returns asetAmountfunction - The Solid main application receive that
setAmountfunction - Each time the input change, Solid main application will call that
setAmountfunction
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>
</>
);
};
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
},
};
};
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:
First, let's install Redux. Remember to run this command in the React component project.
pnpm install @reduxjs/toolkit react-redux
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;
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
]
>
>;
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));
},
};
};
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>
);
};
Let's build the React component library:
pnpm run build
Now, if you run the server, you'll see the following in the screen:
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)