DEV Community 👩‍💻👨‍💻

Cover image for Convert Your SolidJS Component To TypeScript
Matti Bar-Zeev
Matti Bar-Zeev

Posted on

Convert Your SolidJS Component To TypeScript

In this post join me as I convert a simple component written with SolidJS into a TypeScript (TS) SolidJS component. We will go over setting the project’s TypeScript configuration, using the Component type, createSignals and the Accessor type and even solving that annoying style modules typing on the way.

To benefit the most from this post, you should be familiar with TypeScript fundamentals and have a basic understanding of Generics.

As you might know, I have a simple Pagination component made with SolidJS. This is a simple component which displays a set of numbered pages, which you can navigate between:

Image description

It’s currently written in plain JS and looks like this:

import {createEffect, createSignal, For, on} from 'solid-js';
import styles from './Pagination.css';
import {debugComputation} from '@solid-devtools/logger';

const NO_TOTAL_PAGES_ERROR = 'No total pages were given';

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,
   };
}

const Pagination = (props) => {
   const {cursor, totalPages, goNext, goPrev} = paginationLogic(props);
   const [bufferGap, setBufferGap] = createSignal(0);
   const buffer = new Array(props.pagesBuffer).fill(0);

   createEffect(() => {
       debugComputation();
       let newBufferGap = 0;
       if (props.totalPages - cursor() < buffer.length) {
           newBufferGap = props.totalPages - cursor() - buffer.length;
       }
       setBufferGap(newBufferGap);
   });

   return (
       <div>
           <button onClick={goPrev} disabled={cursor() === 0}>
               PREV
           </button>
           <For each={buffer}>
               {(item, index) => {
                   const pageCursor = () => cursor() + index() + bufferGap();
                   const className = () => (pageCursor() === cursor() ? 'selected' : '');

                   return pageCursor() >= 0 && pageCursor() < totalPages ? (
                       <span class={className()}>{` [${pageCursor()}] `}</span>
                   ) : null;
               }}
           </For>
           <button onClick={goNext} disabled={cursor() === totalPages - 1}>
               NEXT
           </button>
       </div>
   );
};

export default Pagination;
Enter fullscreen mode Exit fullscreen mode

What I’d like to do is convert it to TypeScript.
Shouldn’t be hard, right? :)


The approach I’m gonna take here is not replacing the existing component, but rather creating a new one which can “live” alongside the old one. This means that our project should also support plain old javascript.

I start with cloning the component directory, and calling it PaginationTS.
Next step will be changing the extension of the component file to tsx - so now it will be Pagination.tsx.

When I do these changes the Component’s code lights up with errors, but before attending to them, let’s put the new component onto the screen, shall we?
BTW, this project was initialized with the SolidJS scaffolding (see the “getting started” page, but I didn’t use the TS version of it. I change the main App to be App.tsx and add the component to the main App, alongside the old component, and run yarn start...

import Pagination from './components/Pagination/Pagination';
import PaginationTs from './components/PaginationTS/Pagination';
import styles from './App.module.css';
import {attachDebugger} from '@solid-devtools/debugger';

function App() {
   attachDebugger();
   return (
       <div class={styles.App}>
           <Pagination
               totalPages={10}
               pagesBuffer={5}
               onChange={(newCursor) => console.log('newCursor :>> ', newCursor)}
           />

           <PaginationTs
               totalPages={10}
               pagesBuffer={5}
               onChange={(newCursor) => console.log('newCursor :>> ', newCursor)}
           />
       </div>
   );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Yes, both components are active on the same page:

Image description

Take Up Thy Stethoscope And Walk

It’s time to put some TS magic in. The first thing I do is to add a tsconfig.json file with the following:

{
   "compilerOptions": {
       "jsx": "preserve",
       "jsxImportSource": "solid-js",
       "noImplicitAny": true,
       "target": "ES6",
       "moduleResolution": "node",
       "allowJs": true,
       "outDir": "build"
   }
}
Enter fullscreen mode Exit fullscreen mode

Here is what’s going on here real quick -
I instruct the TSC not to mess with JSX and import the types for JSX from solid-js. That, my friends, quiets down all the initial “noise” we have from the TSX component. In addition to that, I’m disallowing implicit “any”s and setting the target compilation to ES6 to be able to use those shiny APIs.
The last part is setting the module resolution to node, so that my modules could look for their dependencies in node_modules (read more about it here).
The “allowJS” allows us to, well, use plain old JS alongside TS. This means our project is in hybrid mode.

Saving this configuration now lights up my SolidJS component with different errors - real TypeScript problems. We can now attend to them starting with this one -

Component props type

Our first error is the implicit “any” type on the component’s props:

Image description

We need to define what type of props the component expects, and this will help us get autocomplete and a sort of validation for the props.
To help us with that we have the “Component” SolidJS type, which is basically a generic type for functional components.
I first define the PaginationProps type:

type PaginationProps = {
   cursor: number;
   initialCursor: number;
   totalPages: number;
   pagesBuffer: number;
   onChange: (value: number) => void;
};
Enter fullscreen mode Exit fullscreen mode

Notice that currently all the properties are required (none is optional) and that the onChange expects to have a value of type number as an argument.

I change my component declaration to use it:

const Pagination: Component<PaginationProps> = (props) => {
Enter fullscreen mode Exit fullscreen mode

And that eliminates the type error I had on the component props, but…
Moving back to the App.tsx and I get new errors, can you guess them?

Image description

Yes, since my component now requires all the props to be available, when I’m not supplying all of them, as you can see in the image above, I get this wonderful error message.
I don’t want to add additional properties when not needed, so I’m going to change the type in order to make them optional, using the “?”:

type PaginationProps = {
   cursor?: number;
   initialCursor?: number;
   totalPages: number;
   pagesBuffer: number;
   onChange?: (value: number) => void;
};
Enter fullscreen mode Exit fullscreen mode

All I’m saying here, that when using this component, it has to have totalPages and pageBuffer. The rest of it? Up to whomever uses it.
Cool thing about it so far is that now I have autocomplete over the Pagination props:

Image description

Moving forward -

Signals

I have a function within my Pagination component, which is responsible for supplying the logic behind the component. You can regard it as a “hook” if you’re coming from the React realms. This function gets some props and returns computed values and methods which then are used for the component’s interaction handling.

Same as before, this function argument has an implicit “any” type to it:

Image description

So fixing it is pretty simple, since the type should be the PaginationProps:

function paginationLogic(props: PaginationProps) {
Enter fullscreen mode Exit fullscreen mode

Let’s dive into the function’s body.
We’re using Solid’s Signals here to make the cursor reactive. Let’s TypeScript that. This is the original line:

const [cursor, setInternalCursor] = createSignal(props.initialCursor || 0);
Enter fullscreen mode Exit fullscreen mode

The value we’re going to store in the signal is of number type, so let’s declare that:

const [cursor, setInternalCursor] = createSignal<number>(props.initialCursor || 0);
Enter fullscreen mode Exit fullscreen mode

Now we can create the PaginationLogicType the function paginationLogic function returns:

type PaginationLogicType = {
   cursor: Accessor<number>;
   totalPages: number;
   goNext: () => void;
   goPrev: () => void;
};
Enter fullscreen mode Exit fullscreen mode

The type we give the cursor property is the Accessor generic with a number type, which basically stands for a function which returns the generic type assigned.
You can read more about signal types here.

Some minor typings

The implicit “any” type points my attention to another method which has an argument with no type - the setCursor method:

Image description

I know that the “newCursor” arg should be a number so let’s type it:

const setCursor = (newCursor: number) => {
       if (newCursor >= 0 && newCursor < props.totalPages) {
           setInternalCursor(newCursor);
       }
   };
Enter fullscreen mode Exit fullscreen mode

Style modules

This one is not directly related to SolidJS, but is still annoying so let’s solve that as well.
In this component I’m using CSS modules, like so:

import styles from './Pagination.module.css';

And TypeScript, well, it does not know how to treat these file extensions. It will simply spit this error message:

"Cannot find module './Pagination.module.css' or its corresponding type declarations.",
Enter fullscreen mode Exit fullscreen mode

The way to solve that is to introduce our own custom types declaration which declares a module which applies to all “*.module.css” files. For that I created a file called “typing.d.ts” on the project’s root, and added this content to it:

declare module '*.module.css' {
   const classes: {[key: string]: string};
   export default classes;
}
Enter fullscreen mode Exit fullscreen mode

That solves this issue and I think that at this point I can safely say that the Pagination component was successfully converted to TypeScript :)

Wrapping up

The Pagination component now supports typings. We went over typing the component function, the signals inside of it, some minor implicit “any” issues and also the annoying style modules type. Here is the final component code, which you can also find on my SolidJs-Lab at Github:

import {Accessor, Component, createEffect, createSignal, For, on} from 'solid-js';
import styles from './Pagination.module.css';
import {debugComputation} from '@solid-devtools/logger';

type PaginationProps = {
   cursor?: number;
   initialCursor?: number;
   totalPages: number;
   pagesBuffer: number;
   onChange?: (value: number) => void;
};

type PaginationLogicType = {
   cursor: Accessor<number>;
   totalPages: number;
   goNext: () => void;
   goPrev: () => void;
};

const NO_TOTAL_PAGES_ERROR = 'No total pages were given';

function paginationLogic(props: PaginationProps): PaginationLogicType {
   if (!props.totalPages) {
       throw new Error(NO_TOTAL_PAGES_ERROR);
   }

   const [cursor, setInternalCursor] = createSignal<number>(props.initialCursor || 0);

   const setCursor = (newCursor: number) => {
       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,
   };
}

const Pagination: Component<PaginationProps> = (props) => {
   const {cursor, totalPages, goNext, goPrev} = paginationLogic(props);
   const [bufferGap, setBufferGap] = createSignal(0);
   const buffer: number[] = new Array(props.pagesBuffer).fill(0);

   createEffect(() => {
       debugComputation();
       let newBufferGap = 0;
       if (props.totalPages - cursor() < buffer.length) {
           newBufferGap = props.totalPages - cursor() - buffer.length;
       }
       setBufferGap(newBufferGap);
   });

   return (
       <div class={styles.pagination}>
           <button onClick={goPrev} disabled={cursor() === 0}>
               PREV
           </button>
           <For each={buffer}>
               {(item, index) => {
                   const pageCursor = () => cursor() + index() + bufferGap();
                   const className = () => (pageCursor() === cursor() ? styles.selected : '');

                   return pageCursor() >= 0 && pageCursor() < totalPages ? (
                       <span class={className()}>{` [${pageCursor()}] `}</span>
                   ) : null;
               }}
           </For>
           <button onClick={goNext} disabled={cursor() === totalPages - 1}>
               NEXT
           </button>
       </div>
   );
};

export default Pagination;
Enter fullscreen mode Exit fullscreen mode

As always, if you think I missed anything, have questions or comments, be sure to share them in the comments section below :)

Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻

Photo by vackground.com on Unsplash

Top comments (3)

Collapse
 
lexlohr profile image
Alex Lohr

Hi. You might want to take a look at our new pagination primitive: github.com/solidjs-community/solid...

I wrote it after seeing this article and thinking about how to solve pagination in an idiomatic, yet unopiniated way.

Collapse
 
mbarzeev profile image
Matti Bar-Zeev

That's cool mate! Basically extracting the pagination logic, much like a "hook" in React. Nice :)
I think that SolidJs shines in that it is very intuitive to extract parts of a component logic this way

Collapse
 
lexlohr profile image
Alex Lohr

Solid comes with a flexibility that will easily seep into your own libraries and primitives if you start to understand the problem you want to solve in the context of Solid and its reactive system.

Pagination is a very nice example, because it consists of only 3 reactive paradigms and not much more:

  • the first/previous/next/last buttons will be disabled if there is no page for them to lead to
  • the current page should have the aria-current attribute set to "true"
  • the "window" of pages should move so that the pages are as evenly spread as possible

You only want solid's reactivity trigger if at least one of those change, so that's exactly what I do:

  • the disabled attribute is a getter that will reactively trigger depending on the page getter and therefore is reactive itself
  • the aria-current attribute is a getter, too
  • the start value that moves the "window" of pages shown is a memo itself and thus will not trigger on every page change, but only if the "window" moves.

A nice touch (even though I say that myself) was the addition of "page" as a non-enumerable property of the props objects: this way we can use a props destructure into the JSX without it polluting the DOM. That's a pattern I will try use more often when working with externalized props.

Build Anything...


Use any Linode offering to create something for the DEV x Linode Hackathon 2022. A variety of prizes are up for grabs, inculding $1,000 USD. 👀

Join the Hackathon <-