Using React Libraries In Svelte
Published Nov 16, 2024
Table of Contents
- Using React Libraries In Svelte
- Understanding How React Works
- Rendering React Components In Svelte
- Nesting Elements In React
- Reusing The Svelte Component
Using React Libraries In Svelte
I love using the virtual whiteboard app Excalidraw, so I was excited to learn it was also open source, but unfortunately it’s only made for React which means you can’t use it inside Svelte, or does it?
You can find the code on GitHub.
Every JavaScript framework is a single page application that lives in a random <div>
somewhere on the page using mount from Svelte or createRoot from React, so nothing stops you from using another framework elsewhere in your app:
<div class="app"></div>
However, the problem is that some JavaScript frameworks require a compilation step before they can be used, but React is uniquely positioned so it doesn’t.
Understanding How React Works
The ironic part about React comes from the name because it’s not reactive in a sense that only the value you updated is going to change, but instead React is lazy, and it’s going to re-render the entire component:
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
// this logs each time you update count
console.log(count)
return (
<button onClick={() => setCount(count + 1)}>
You pressed me {count} times
</button>
)
}
What’s more interesting is that React uses a XML-like syntax named JSX for the template so everything gets turned into a function call:
// JSX
let div = <div>JSX</div>
// output
let div = createElement('div', null, 'JSX')
Here is the signature of the createElement function:
createElement(type, props, ...children)
As you can see, “it’s just JavaScript” so you don’t need to even transpile the code to JavaScript and you can write it by hand:
import React, { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
return React.createElement(
'button',
{ onClick: setCount(count + 1) },
'You pressed me ',
count,
' times'
)
}
Rendering React Components In Svelte
I’m using Excalidraw in this example, so I’m going to install the required dependencies inside a SvelteKit project:
npm i react react-dom @types/react @types/react-dom @excalidraw/excalidraw
If you’re following along, you should also include this from the Excalidraw docs inside your Vite config:
export default defineConfig({
// ...
define: {
'process.env.IS_PREACT': JSON.stringify('true'),
}
})
I’m going to create a Excalidraw.svelte
component inside routes
and import it inside +page.svelte
:
<script lang="ts">
import Excalidraw from './Excalidraw.svelte'
</script>
<Excalidraw />
Let’s mount the React app inside the root element, and we have to use a dynamic import for Excalidraw because it would blow up during server-side rendering:
<script lang="ts">
import { createElement } from 'react'
import { createRoot } from 'react-dom/client'
import type { ExcalidrawProps } from '@excalidraw/excalidraw/types/types'
let props: ExcalidrawProps = $props()
let rootEl: HTMLElement
$effect(() => {
const root = createRoot(rootEl)
import('@excalidraw/excalidraw').then(({ Excalidraw }) => {
const excalidraw = createElement(Excalidraw, { ...props })
root.render(excalidraw)
})
return () => root.unmount()
})
</script>
<div bind:this={rootEl} class="root"></div>
<style>
.root {
height: 100svh;
}
</style>
⚠️ The reason we don’t use
await
inside$effect
is because if you pass an async function to$effect
, the cleanup is never going to be called.
Nesting Elements In React
You can pass an array of children to a React element if you want to nest elements. Let’s customize Excalidraw by including a welcome screen and changing the main menu:
$effect(() => {
const root = createRoot(rootEl)
import('@excalidraw/excalidraw').then(({ Excalidraw, WelcomeScreen, MainMenu }) => {
const welcome = createElement(WelcomeScreen, { key: 'WelcomeScreen' })
const menu = createElement(MainMenu, { key: 'MainMenu' }, [
createElement(MainMenu.DefaultItems.LoadScene, { key: 'LoadScene' }),
createElement(MainMenu.DefaultItems.SaveAsImage, { key: 'SaveAsImage' }),
createElement(MainMenu.DefaultItems.Export, { key: 'Export' }),
createElement(MainMenu.Separator, { key: 'Separator' }),
createElement(MainMenu.DefaultItems.ChangeCanvasBackground, { key: 'ChangeCanvasBackground' }),
])
const excalidraw = createElement(Excalidraw, { ...props }, [welcome, menu])
root.render(excalidraw)
})
return () => root.unmount()
})
Reusing The Svelte Component
You now have a reusable Svelte component, and you could also export what you need from the component like the Excalidraw API:
<script lang="ts">
import Excalidraw from './Excalidraw.svelte'
import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'
let excalidrawAPI: ExcalidrawImperativeAPI | undefined = $state()
$effect(() => {
excalidrawAPI?.onChange(console.log)
})
</script>
<Excalidraw theme="dark" excalidrawAPI={api => excalidrawAPI = api} />
Keep in mind, using React is like bringing a sledgehammer to your project when there might be other alternatives, so use it only if you must.