https://www.jackfranklin.co.uk/blog/comparing-svelte-and-react-javascript/ Jack Franklin * Blog * Speaking * Twitter * RSS March 9, 2021 Comparing Svelte and React Last year I created Pomodone, a small time tracking application based on the Pomodoro technique of working in 25 minute intervals. It's a pretty basic app; it has a 25 minute timer (that runs in a Web Worker) and saves a history of your "poms" to a small Firebase database. I initially built it using React (well, Preact actually) but I then started to play around with Svelte, and decided rebuilding the app in Svelte might be a nice way to blog about the similarities and differences between the libraries. This is not a post declaring Svelte to be better than React, or vice-versa. This is a post where I'll tell you about my preferences, and what I find easier or harder with either framework. I'm not here to pick a fight! Plus, Pomodone is hardily a vastly complex application that could be used to fully put React or Svelte through its paces. Think of this post as a commentary based on my experience throwing a side project together, focusing on the developer experience putting these components together. Authentication The app uses Firebase Authentication to log a user in via either their GitHub or Google account. I love Firebase Authentication, it's such an easy way to add auth to side projects. React's hooks are a great way to package this up; I create a useCurrentUser hook which listens out to authentication changes and sets some state accordingly. I can then trust React to re-render as required when an authentication change is noted. export const useCurrentUser = () => { const [currentUser, setCurrentUser] = useState(undefined) useEffect(() => { return firebase.auth().onAuthStateChanged((details) => { setCurrentUser( details ? { displayName: details.displayName, provider: { 'google.com': 'Google', 'github.com': 'GitHub', }[details.providerData[0].providerId], uid: details.uid, } : null ) }) }, []) return [currentUser] } Within any component, I can write: const [currentUser] = useCurrentUser() This is nice; it's low effort and lets any component quickly access the current user. The only downside of this is that you potentially have many onAuthStateChanged listeners; I could mitigate this by only binding one listener, or by putting the current user in a context instead. Talking of context, that's much closer to the approach I take with Svelte and use a writable store. export const currentUser = writable() export const listenForAuthChanges = () => { return firebase.auth().onAuthStateChanged((details) => { if (details) { currentUser.set({ displayName: details.displayName, provider: { 'google.com': 'Google', 'github.com': 'GitHub', }[details.providerData[0].providerId], uid: details.uid, }) } else { currentUser.set(null) } }) } Within the top level Svelte component, I can call this within onMount, which will run once when the component is mounted (the function is returned so we unsubscribe when the component is removed, much like how useEffect lets you return a function). onMount(() => { return listenForAuthChanges() }) Now anywhere in my Svelte codebase, a component can import the currentUser writable store, and act accordingly. What I like is that currentUser isn't a value, it's a store, and therefore you have full control over how you deal with it. You can either subscribe to it and manually control with state changes: currentUser.subscribe(newValue => { ... }) Or, if you want to just read the latest value, you can prefix it with a $: console.log($currentUser) This is where some of Svelte's syntax trickery begins to shine; this dollar prefix trick automatically subscribes you to the store's latest value. I both like and dislike this; it's a nice syntax once you know it, but it's a bit odd as a beginner to get used to. However I like that Svelte doesn't make me use the subscribe API every time I need to read the value. As far as basic authentication goes, both libraries seem to take similar approaches here. Whilst the terminology and exact syntax differs slightly, both allow you to subscribe to a Firebase listener and get updated when the authentication state changes. React's contexts and Svelte's stores play almost identical roles for their library. Using a worker Pomodone has to keep a 25 minute timer going and try to be as accurate as possible. If a browser tab is backgrounded (e.g., not the focused tab), most browsers will lower the priority of its setTimeout calls and not run them strictly to time. Most of the time on the web this isn't a massive deal, but when a user is tracking 25 minutes of work via your app, it is! Plus, over the course of 25 minutes, any slight time drift will cause the final time to be quite far off. However, if these timeouts are moved into a web worker, they should run to time and not get de-prioritised by the browser. Therefore, in my Tracker component, I need to instantiate a web worker, send it messages and receive data (such as time remaining) back. This is one area where I found React more "admin heavy" than Svelte; because React components are re-executed every time the component re-renders, you can easily end up with thousands of workers being created! It's essential to use useRef to avoid this problem by maintaining a reference to the worker that you've created. Firstly I set up the initial state I need for the component: const [currentPom, setCurrentPom] = useState(null) const [currentUser] = useCurrentUser() const worker = useRef(null) And then create a useEffect hook that will instantiate the worker, if required, and bind an event listener to listen for messages: useEffect(() => { if (!worker.current) { worker.current = new Worker(workerURL) window.worker = worker.current } const onMessage = (event) => { if (event.data.name === 'tick') { setCurrentPom((currentPom) => ({ ...currentPom, secondsRemaining: event.data.counter, })) } else if (event.data.name === 'start') { // More branches removed here to save space... } } worker.current.addEventListener('message', onMessage) return () => { worker.current.removeEventListener('message', onMessage) } }, [currentUser]) And then, when the user hits the "Start" button, I have to send the worker a message: const onStartPom = () => { if (!worker.current) return worker.current.postMessage('startTimer') } Svelte looks pretty similar, but has two small changes that personally make the Svelte code easier to read, in my opinion: 1. We don't have to keep the worker in useRef, and can just assign it to a variable. 2. We can pull the event listener code out into a function more easily, as that function won't then become a dependency to a useEffect - at which point we will have to wrap it in useCallback. Instantiating the worker is now: let worker onMount(() => { worker = new Worker(workerURL) worker.addEventListener('message', onWorkerMessage) return () => { worker.removeEventListener('message', onWorkerMessage) } }) We also don't have to set state by using React's setX(oldX => newX) convention, and can just mutate the local variable: function onWorkerMessage(event) { if (event.data.name === 'tick') { currentPom = { ...currentPom, secondsRemaining: event.data.counter, } } else if (event.data.name === 'start') { // More branches here removed to save space... } } Here's where I start to have a preference for Svelte; the two are very similar but once I got used to Svelte I found that React felt like jumping through hoops. You can't create a worker instance, it has to go in a useRef, and then you can't easily pull code out into a function without then requiring useCallback so it can be a safe dependency on useEffect. With Svelte I write code that's closer to "plain" JavaScript, whereas in React more of my code is wrapped in a React primitive. Conditional rendering One part of React that I've always championed is how it's just JavaScript. I like that in React you don't use a distinct template syntax and instead embed JavaScript, compared to Svelte's templating language:
hello world!
hello world!
world!
} /> } One gripe I've had with this approach is that you lose the visual cues that you're passing children into the Box component; they now aren't nested within the Box when you render them like we're used to in HTML; it's now up to you to read the props and spot which ones are being used to provide children. It's easily done on this dummy example, but harder in "real world" applications - or at least, I find it harder! Svelte's approach is to define multiple slots with explicit names to let the user provide the elements that should fill those slots:hello
world!