Do you prefer JavaScript or TypeScript?
Are you coming from any of the following frameworks?

Comparison with React

React has had a big influence on Solid. Its unidirectional flow and explicit segregation of read and write in its Hooks API informed Solid's API. More so than the objective of being just a "Render Library" rather than a framework. Solid has strong opinions on how to approach managing data in application development but doesn't seek to constrain its execution.

However, as much as Solid aligns with React's design philosophy, it works fundamentally differently. This API similarity with a different execution model usually throws off React developers that expect certain React patterns to be transferable to Solid. This guide is here to help point out those differences and how to overcome them.

Components as render functions vs setup functions

React

In a simplified explanation, React components work as a way to organize your code and handle view updates derive from state or props changes. This means that every time there's an update, React wil re-run the component with the latest state/props to reflect it in the view. Let's take a look at the following example:

jsx
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<p>Count is: {count}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};
jsx
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<p>Count is: {count}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};

Whenever we click our button, we will trigger a state update resulting in a re-run of our component.

A screenshot of the file browser in Stackblitz, displaying all of the files in the template

Solid

We can create extremely similar code with Solid, but the way it works under the hood will be different. Again, the component will be useful as a way to organize the code, but unlike React it won't re-run everytime there's a state or props update.

Instead, the component will setup everything that needs to be tracked by Solid's reactive system and move out of the way once the code is being executed. This means that the function component will run only once as it won't handle state/props updates. Here's the same example written in Solid:

jsx
import { createSignal } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};
jsx
import { createSignal } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};

When we click our button only granular updates will be executed by Solid's reactive system.

A screenshot of the file browser in Stackblitz, displaying all of the files in the template

This is what we mean in the title by render functions vs setup functions. In React, rendering and its updates are tied to the component. In Solid however, components only exists in your code and run once to setup all the pieces needed for the reactive system to take place. You can think of components in Solid as vanishing components, they go away when the code is executed.

This leads to discrepancies on how code works between the two frameworks, despite of their similarity when you write code. For one, we can move state out of the component and Solid's code will still work.

jsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
const App = () => {
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};
jsx
import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);
const App = () => {
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};

Again, this is possible because Solid's reactive system lives outside of components. If you want further reading on the topic, check out Ryan Carniato's (creator of Solid) article Components are Pure Overhead

Early returns and the use of <Show>

Since components in Solid are executed only once, the following React code is not transferable to Solid

React

jsx
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
if (count > 5) {
return <p>Count is too high!</p>;
}
return (
<>
<p>Count is: {count}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};
jsx
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
if (count > 5) {
return <p>Count is too high!</p>;
}
return (
<>
<p>Count is: {count}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};

In the example above, since React re-renders on every state change, our condition is evaluated every time we click the button. Once count is bigger than 5 our condition will be true, and React will render our <p element.

Solid

We can write similar code, but once again, the execution model is different

jsx
import { createSignal } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
if (count() > 5) {
return <p>Count is too high!</p>;
}
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};
jsx
import { createSignal } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
if (count() > 5) {
return <p>Count is too high!</p>;
}
return (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
};

Here, since we initialize our count at 0 and the component only runs once, the condition will be evaluated, turn out to be false, and never be re-evaluated. You can click the button and get past 5, and you'll still see the counter and button.

This is due to how Solid's granular reactivity works. Put in simple terms, React is opt-out rendering meaning that React will re-render everything if not told otherwise (think memo(), useMemo(), useCallback()).

On the other hand, Solid is opt-in rendering meaning it will not re-render anything that is not tracked by the reactive system. Solid thinks of rendering elements to the DOM as a side effect of state changes, so the way to "fix" our code is to check our condition inside our JSX to make the reactive system track it and derive the DOM output as a result of state change. Solid provides a built-in component to help us with that: <Show>

jsx
import { createSignal, Show } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
const fallback = (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
return (
<Show when={count() > 5} fallback={fallback}>
<p>Count is too high!</p>
</Show>
);
};
jsx
import { createSignal, Show } from "solid-js";
const App = () => {
const [count, setCount] = createSignal(0);
const fallback = (
<>
<p>Count is: {count()}</p>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Increase count by 1
</button>
</>
);
return (
<Show when={count() > 5} fallback={fallback}>
<p>Count is too high!</p>
</Show>
);
};

Going through lists and the use of <For>

Usually the way to iterate through lists in React is the following

jsx
import { useState } from "react";
const App = () => {
const [todos] = useState([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
</>
);
};
jsx
import { useState } from "react";
const App = () => {
const [todos] = useState([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
</>
);
};

While this will also work on Solid, the idiomatic way to render lists is by using <For> component

jsx
import { createSignal, For } from "solid-js";
const App = () => {
const [todos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<ul>
<For each={todos()}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
</>
);
};
jsx
import { createSignal, For } from "solid-js";
const App = () => {
const [todos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<ul>
<For each={todos()}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
</>
);
};

One benefit of the component is that rendered items are keyed by default, so we can forget about doing it ourselves.

Keys by default, allow granular updates as Solid will only apply updates to specific items in the list rather than recreating the list on every update.

Props destructuring

Prop destructuring is a common thing to do in React. If we were to click the "Add Todo" button on the code below, React would re-render the <App> component and all of its children, thus displaying the new value on the DOM.

jsx
// This will update the DOM with the new todo
import { useState } from "react";
const Todos = ({ todos }) => {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
);
};
const App = () => {
const [todos, setTodos] = useState([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};
jsx
// This will update the DOM with the new todo
import { useState } from "react";
const Todos = ({ todos }) => {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.name}</li>
))}
</ul>
);
};
const App = () => {
const [todos, setTodos] = useState([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};

Having similar code for Solid, and destructuring props on <Todos> component, will fail to update the DOM.

jsx
// This won't update the DOM with the new todo
import { createSignal, For } from "solid-js";
const Todos = ({ todos }) => {
return (
<ul>
<For each={todos}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
);
};
const App = () => {
const [todos, setTodos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos()} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};
jsx
// This won't update the DOM with the new todo
import { createSignal, For } from "solid-js";
const Todos = ({ todos }) => {
return (
<ul>
<For each={todos}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
);
};
const App = () => {
const [todos, setTodos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos()} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};

The reason for this, again, is that Solid has an opt-in reactive model. This means that anything that's not inside a tracking scope (Solid primitives and JSX) won't trigger an update.

In our code above, by destructuring the props we are accessing them in an un-tracked scope. To "fix" this we would have to access our props in a tracked scope, in this case inside JSX

jsx
// This will update the DOM with the new todo
import { createSignal, For } from "solid-js";
const Todos = (props) => {
return (
<ul>
<For each={props.todos}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
);
};
const App = () => {
const [todos, setTodos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos()} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};
jsx
// This will update the DOM with the new todo
import { createSignal, For } from "solid-js";
const Todos = (props) => {
return (
<ul>
<For each={props.todos}>{(todo) => <li>{todo.name}</li>}</For>
</ul>
);
};
const App = () => {
const [todos, setTodos] = createSignal([
{ id: "1", name: "Learn Solid" },
{ id: "2", name: "Learn Solid Start" },
]);
return (
<>
<h1>TO-DO</h1>
<Todos todos={todos()} />
<button
onClick={() =>
setTodos((prev) => [...prev, { id: "3", name: "Learn Qwik" }])
}
>
Add Todo
</button>
</>
);
};

React vs. Solid

Here's a table differentiating hooks in React against hooks in Solid.

React (Class components)React (Functional components)Solid
componentDidMount()useEffect(() => {}, [])onMount()
componentWillUnmount()useEffect(() => { return () => {}}, [])onCleanup()
this.stateuseState()createSignal()
this.stateuseState()createStore()
N/AuseMemo()createMemo()
componentDidUpdate()useEffect(() => {}, [dependencies])createEffect()