Join me in this post as I migrate a React component with hooks to SolidJS.
I recently have been hearing more and more about SolidJS and after reading a bit about it and listening to several podcasts with its creator, Ryan Carniato, I got really excited by what this Framework offers with a tingling feeling on my finger tips urging me to have a go at it.
Not to diss anyone, but it seems to me that React has become this complex state machine, with many patches when SolidJS appears to offer a simple approach which is very intuitive and makes a lot of sense right away.
In this post I will attempt to take a simple React component, convert it to SolidJS and learn on the path if that’s really the case, and if it really shines where React does not.
The component I’m going to take is my dreadfully “skinny” Pagination component, which resides on my @pedalboard/components package and looks like this:
It uses a Pagination hook which encapsulates the cursor and onChange callback logics. I believe that it is a good candidate to stretch SolidJS limits a bit from the usual “Hello World” examples.
Are we all set? Let’s get to it
I first go to the SolidJS docs and see what it takes to get things started. Looking at the “new docs” I’m going for the JavaScript template.
“Installing” everything using degit (a tool for coping git repos by Rich Harris), I ran yarn start
and I have a SolidJS application ready to go. It actually has the spinning logo much like the Create-React-App (CRA) and as I understand, Ryan is no shy about the inspiration he got from the framework.
For starters I like the fact that unlike CRA there aren’t a ton of configuration files when the project is set. Perhaps it is due to the fact the CRA supports much more features and boilerplate code, but I like the simplicity so far.
My Pagination component origin code can be found here. So first thing I will do is create my component files structure:
My Pagination.jsx
component has this code to begin with:
const Pagination = () => {
return <div>Pagination Component</div>;
};
export default Pagination;
And in the App.jsx
I will remove all the initial code the scaffold comes with and place my component in there instead:
import Pagination from './components/Pagination/Pagination';
function App() {
return (
<div>
<Pagination />
</div>
);
}
export default App;
That’s a good start, Vite is truly blazing fast and I get my result in the browser swiftly - a mere text saying “Pagination component”. Moving on.
I’m copying the component content from my React component into the SolidJS one, without the usage of the Pagination hook yet. I just want to see if this compiles well. Here is the code now:
const Pagination = (props) => {
const {cursor, totalPages, goPrev, goNext} = {cursor: 0, totalPages: 10, goPrev: () => {}, goNext: () => {}};
const buffer = new Array(props.pagesBuffer).fill(0);
let bufferGap = 0;
if (totalPages - cursor < buffer.length) {
bufferGap = totalPages - cursor - buffer.length;
}
return (
<div>
<button onClick={goPrev} disabled={cursor === 0}>
PREV
</button>
{buffer.map((item, index) => {
const pageCursor = cursor + index + bufferGap;
const className = pageCursor === cursor ? 'selected' : '';
return pageCursor >= 0 && pageCursor < totalPages ? (
<span key={`page-${pageCursor}`} className={className}>
{` [${pageCursor}] `}
</span>
) : null;
})}
<button onClick={goNext} disabled={cursor === totalPages - 1}>
NEXT
</button>
</div>
);
};
export default Pagination;
In our App.jsx code we will add the pagesBuffer, like so:
function App() {
return (
<div class={styles.App}>
<Pagination pagesBuffer={5} />
</div>
);
}
And the result looks like this now:
That’s not bad at all, right? No real changes to the code, which I consider some of the immediate advantages of SolidJS if you’re coming from a React background. The syntax stays the same for the most part.
Now we need to take care of what the hook is providing us, which is basically the whole cursor manipulation. Looking at the hook’s code, how do I migrate it to SolidJS?
I think it would be wise to start with the basic state it has and the methods which manipulate it. This is how the code look like in the origin hook:
if (!totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setInternalCursor] = useState(initialCursor || 0);
const setCursor = (newCursor) => {
if (newCursor >= 0 && newCursor < totalPages) {
setInternalCursor(newCursor);
}
};
const goNext = () => {
const nextCursor = cursor + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor - 1;
setCursor(prevCursor);
};
I will use SolidJS createSignal in order to create the cursor state. This means that in any place where I have a reference to the cursor
I will need to change it to be cursor()
.
I’m also removing the code which uses the hook, and so my SolidJS component looks like this now -
import {createSignal} from 'solid-js';
const Pagination = (props) => {
if (!props.totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setInternalCursor] = createSignal(props.initialCursor || 0);
const setCursor = (newCursor) => {
if (newCursor >= 0 && newCursor < props.totalPages) {
setInternalCursor(newCursor);
}
};
const goNext = () => {
const nextCursor = cursor() + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor() - 1;
setCursor(prevCursor);
};
const buffer = new Array(props.pagesBuffer).fill(0);
let bufferGap = 0;
if (props.totalPages - cursor() < buffer.length) {
bufferGap = props.totalPages - cursor() - buffer.length;
}
return (
<div>
<button onClick={goPrev} disabled={cursor() === 0}>
PREV
</button>
{buffer.map((item, index) => {
const pageCursor = cursor() + index + bufferGap;
const className = pageCursor === cursor() ? 'selected' : '';
return pageCursor >= 0 && pageCursor < props.totalPages ? (
<span key={`page-${pageCursor}`} className={className}>
{` [${pageCursor}] `}
</span>
) : null;
})}
<button onClick={goNext} disabled={cursor() === props.totalPages - 1}>
NEXT
</button>
</div>
);
};
export default Pagination;
Let’s also add the CSS for this component so that we can see the current cursor, in Pagination.css
:
.selected {
font-weight: bolder;
}
And import it to the component as style module
import {createSignal} from 'solid-js';
import styles from './Pagination.css';
const Pagination = (props) => {
if (!props.totalPages) {
. . .
And we’re getting there:
But here is something interesting which represents one of the key differences between React and SolidJS - As you can see I’m calculating the bufferGap
on each render of the React component so I will not end up with displaying less pages in the buffer than what the component is required to.
In other words avoid this situation:
Where the result we want is this:
The value which determines this behavior is the bufferGap and the reason we have this bug now is that SoliJS does not re-runs the component function over and over again forcing the bufferGap to recalculate according to the new state. It calls the component’s function just once.
So to solve that I create a new signal, called “bufferGap” and I use the createEffect SolidJS method to “listen” for changes over the cursor() and calculate the bufferGap accordingly:
const [bufferGap, setBufferGap] = createSignal(0);
createEffect(() => {
let newBufferGap = bufferGap();
if (props.totalPages - cursor() < buffer.length) {
newBufferGap = props.totalPages - cursor() - buffer.length;
}
setBufferGap(newBufferGap);
});
Notice that I don’t need to put anything in a dependency array - Solid knows to inspect the function body and when it detects a signal in it (like our cursor) it will know to invoke this method again when it changes.
Down the code I’m using my newly created state, like so:
const pageCursor = cursor() + index + bufferGap();
I could do this with the Derived State capability of solid, but in my particular case having it like this ensures that the bufferGap calculation will be called just once for each time the cursor changes.
Moving forward we would like our component to invoke an onChange
callback when the cursor changes with the new cursor as an argument.
I’m creating another effect which will invoke the onChange callback when ever the cursor changes (I could probably consolidate it with the previous createEffect but I like the separation here better):
createEffect(() => {
props.onChange?.(cursor());
});
And in the application using this component I add the actual callback:
<Pagination
totalPages={10}
pagesBuffer={5}
onChange={(newCursor) => console.log('newCursor :>> ', newCursor)}
/>
This cannot be any simpler, right?
Yes, but we have an issue here - when the components first renders it calls the onChange callback, though there was no real change, and we solved that issue in the React component using a ref which indicates if the hook is initializing, which then means it does not need to trigger the callback, but how do we solve it here?
turns out there is a great API called "on" for SolidJS which allows to invoke a callback function once a signal has changed. The really cool thing about it is that it can be deferred and not invoke the function when the value is first set.
Here how it will look in the code:
createEffect(on(cursor, (value) => props.onChange?.(value), {defer: true}));
Thanks @uminer for this great advice!
We’ve reached a fine milestone here. We have a Pagination component in SolidJS which does exactly what our origin React component did, but with a slight difference -
We don’t have the cursor logic represented as a reusable hook. Can we do that in SolidJS?
Let’s extract it all to a function:
function paginationLogic(props) {
if (!props.totalPages) {
throw new Error(NO_TOTAL_PAGES_ERROR);
}
const [cursor, setInternalCursor] = createSignal(props.initialCursor || 0);
const setCursor = (newCursor) => {
if (newCursor >= 0 && newCursor < props.totalPages) {
setInternalCursor(newCursor);
}
};
const goNext = () => {
const nextCursor = cursor() + 1;
setCursor(nextCursor);
};
const goPrev = () => {
const prevCursor = cursor() - 1;
setCursor(prevCursor);
};
createEffect(on(cursor, (value) => props.onChange?.(value), {defer: true}));
return {
cursor,
totalPages: props.totalPages,
goNext,
goPrev,
};
}
And our component will use it like so:
const Pagination = (props) => {
const {cursor, totalPages, goNext, goPrev} = paginationLogic(props);
const buffer = new Array(props.pagesBuffer).fill(0);
const [bufferGap, setBufferGap] = createSignal(0);
createEffect(() => {
let newBufferGap = bufferGap();
if (props.totalPages - cursor() < buffer.length) {
newBufferGap = props.totalPages - cursor() - buffer.length;
}
setBufferGap(newBufferGap);
});
return (
<div>
<button onClick={goPrev} disabled={cursor() === 0}>
PREV
</button>
{buffer.map((item, index) => {
const pageCursor = cursor() + index + bufferGap();
const className = pageCursor === cursor() ? 'selected' : '';
return pageCursor >= 0 && pageCursor < totalPages ? (
<span key={`page-${pageCursor}`} className={className}>
{` [${pageCursor}] `}
</span>
) : null;
})}
<button onClick={goNext} disabled={cursor() === totalPages - 1}>
NEXT
</button>
</div>
);
};
This is exactly like a React hook!
I can now take this function, export it as a separated module, and have it reused across my components and applications.
This is freaking awesome!
Wrapping up
So here we have it - we took a React component which uses a hook and converted it to Solid JS in what appears to be very a intuitive, and above all, simple process
I’m really excited about SolidJS - the fact that it is very small in size, performant while going back to the roots of web development makes it a good candidate for being the next evolution in Frontend development IMO. I know that there are still a lot of aspects which React covers which SolidJS still needs to catch up with, but SolidJS comes with the right approach to matters as I see it.
As always if you have any comments on how this can be done better or questions be sure to leave them in the comments below
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻
Top comments (5)
Regarding the
cursor()
issue, you might want to look at theon
utility (and it'sdeferred
option).This is a great advice! I will update my article shortly with a working example.
I'm not familiar with such functionality in React - do you?
No, not that I'm aware of. Interestingly, it is only possible for deferred to work with an explicit dependency array like react. Solid needs to know which signals to track, which it usually does by running the effect once and tracking the signals read - something it resets and does every time. The
on
let's you specify explicitly, so it becomes possible to use deferred.Small suggestion about calculating the
bufferGap
value:I understand that it is probably here for the sake of example of createEffect usage, but you could avoid having unnecessary rerender caused when updating
bufferGap
state by storing the state dependant value in a ref. It could look something like this:Because we store the value in a ref, it allows us to perform all calculations without cascading state updates. We would avoid unnecessary calculations since we only update the ref value the condition is met.
I believe something like this should be possible in Solid
This is where SolidJS shines over React IMO (at least one of the main places).
The component function is called only once. There will be no unnecessary renders.
No need for ref. You know exactly the state of your component as it only reacts to changes and not renders in its entirety.
This is freaking awesome, and sooooo simple to reason about.