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
mount
now accepts a new argument: a function calledonIncrement
- The main Solid app pass a function
onIncrement
to themount
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
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
mount
function returns asetAmount
function - The Solid main application receive that
setAmount
function - Each time the input change, Solid main application will call that
setAmount
function
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)